1/****************************************************************************** 2 QtAV: Multimedia framework based on Qt and FFmpeg 3 Copyright (C) 2012-2016 Wang Bin <wbsecg1@gmail.com> 4 5* This file is part of QtAV (from 2013) 6 7 This library is free software; you can redistribute it and/or 8 modify it under the terms of the GNU Lesser General Public 9 License as published by the Free Software Foundation; either 10 version 2.1 of the License, or (at your option) any later version. 11 12 This library is distributed in the hope that it will be useful, 13 but WITHOUT ANY WARRANTY; without even the implied warranty of 14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 Lesser General Public License for more details. 16 17 You should have received a copy of the GNU Lesser General Public 18 License along with this library; if not, write to the Free Software 19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 20******************************************************************************/ 21//if qt<5.3, remove lines: sed '/\/\/IF_QT53/,/\/\/ENDIF_QT53/d' 22import QtQuick 2.0 23//IF_QT53 24import QtQuick.Dialogs 1.2 25/* 26//ENDIF_QT53 27import QtQuick.Dialogs 1.1 /* 28*/ 29//import QtMultimedia 5.0 30import QtAV 1.7 31import QtQuick.Window 2.1 32import "utils.js" as Utils 33 34Rectangle { 35 id: root 36 layer.enabled: false //VideoOutput2 can not update correctly if layer.enable is true. default is false 37 objectName: "root" 38 width: Utils.scaled(800) 39 height: Utils.scaled(450) 40 color: "black" 41 signal requestFullScreen 42 signal requestNormalSize 43 44 function init(argv) { 45 console.log("init>>>>>screen density logical: " + Screen.logicalPixelDensity + " pixel: " + Screen.pixelDensity); 46 } 47 48 VideoFilter { 49 id: negate 50 type: VideoFilter.GLSLFilter 51 shader: Shader { 52 postProcess: "gl_FragColor.rgb = vec3(1.0-gl_FragColor.r, 1.0-gl_FragColor.g, 1.0-gl_FragColor.b);" 53 } 54 } 55 VideoFilter { 56 id: hflip 57 type: VideoFilter.GLSLFilter 58 shader: Shader { 59 sample: "vec4 sample2d(sampler2D tex, vec2 pos, int p) { return texture(tex, vec2(1.0-pos.x, pos.y));}" 60 } 61 } 62 63 VideoOutput2 { 64 id: videoOut 65 opengl: true 66 fillMode: VideoOutput.PreserveAspectFit 67 anchors.fill: parent 68 source: player 69 orientation: 0 70 property real zoom: 1 71 //filters: [negate, hflip] 72 SubtitleItem { 73 id: subtitleItem 74 fillMode: videoOut.fillMode 75 rotation: -videoOut.orientation 76 source: subtitle 77 anchors.fill: parent 78 } 79 Text { 80 id: subtitleLabel 81 rotation: -videoOut.orientation 82 horizontalAlignment: Text.AlignHCenter 83 verticalAlignment: Text.AlignBottom 84 font: PlayerConfig.subtitleFont 85 style: PlayerConfig.subtitleOutline ? Text.Outline : Text.Normal 86 styleColor: PlayerConfig.subtitleOutlineColor 87 color: PlayerConfig.subtitleColor 88 anchors.fill: parent 89 anchors.bottomMargin: PlayerConfig.subtitleBottomMargin 90 } 91 } 92 93 MediaPlayer { 94 id: player 95 objectName: "player" 96 //loops: MediaPlayer.Infinite 97 //autoLoad: true 98 autoPlay: true 99 videoCodecPriority: PlayerConfig.decoderPriorityNames 100 onPositionChanged: control.setPlayingProgress(position/duration) 101 videoCapture { 102 autoSave: true 103 onSaved: { 104 msg.info("capture saved at: " + path) 105 } 106 } 107 onSourceChanged: { 108 videoOut.zoom = 1 109 videoOut.regionOfInterest = Qt.rect(0, 0, 0, 0) 110 msg.info("url: " + source) 111 } 112 113 onDurationChanged: control.duration = duration 114 onPlaying: { 115 control.mediaSource = player.source 116 control.setPlayingState() 117 if (!pageLoader.item) 118 return 119 if (pageLoader.item.information) { 120 pageLoader.item.information = { 121 source: player.source, 122 hasAudio: player.hasAudio, 123 hasVideo: player.hasVideo, 124 metaData: player.metaData 125 } 126 } 127 } 128 onSeekFinished: { 129 console.log("seek finished " + Utils.msec2string(position)) 130 } 131 132 onInternalAudioTracksChanged: { 133 if (typeof(pageLoader.item.internalAudioTracks) != "undefined") 134 pageLoader.item.internalAudioTracks = player.internalAudioTracks 135 } 136 onExternalAudioTracksChanged: { 137 if (typeof(pageLoader.item.externalAudioTracks) != "undefined") 138 pageLoader.item.externalAudioTracks = player.externalAudioTracks 139 } 140 onInternalSubtitleTracksChanged: { 141 if (typeof(pageLoader.item.internalSubtitleTracks) != "undefined") 142 pageLoader.item.internalSubtitleTracks = player.internalSubtitleTracks 143 } 144 145 onStopped: control.setStopState() 146 onPaused: control.setPauseState() 147 onError: { 148 if (error != MediaPlayer.NoError) { 149 msg.error(errorString) 150 } 151 } 152 muted: control.mute // TODO: control from system 153 volume: control.volume 154 onVolumeChanged: { //why need this? control.volume = player.volume is not enough? 155 if (Math.abs(control.volume - volume) >= 0.01) { 156 control.volume = volume 157 } 158 } 159 onStatusChanged: { 160 if (status == MediaPlayer.Loading) 161 msg.info("Loading " + source) 162 else if (status == MediaPlayer.Buffering) 163 msg.info("Buffering") 164 else if (status == MediaPlayer.Buffered) 165 msg.info("Buffered") 166 else if (status == MediaPlayer.EndOfMedia) 167 msg.info("End") 168 else if (status == MediaPlayer.InvalidMedia) 169 msg.info("Invalid") 170 } 171 onBufferProgressChanged: { 172 msg.info("Buffering " + Math.floor(bufferProgress*100) + "%...") 173 } 174 // onSeekFinished: msg.info("Seek finished: " + Utils.msec2string(position)) 175 } 176 Subtitle { 177 id: subtitle 178 player: player 179 enabled: PlayerConfig.subtitleEnabled 180 autoLoad: PlayerConfig.subtitleAutoLoad 181 engines: PlayerConfig.subtitleEngines 182 delay: PlayerConfig.subtitleDelay 183 fontFile: PlayerConfig.assFontFile 184 fontFileForced: PlayerConfig.assFontFileForced 185 fontsDir: PlayerConfig.assFontsDir 186 187 onContentChanged: { //already enabled 188 if (!canRender || !subtitleItem.visible) 189 subtitleLabel.text = text 190 } 191 onLoaded: { 192 msg.info(qsTr("Subtitle") + ": " + path.substring(path.lastIndexOf("/") + 1)) 193 console.log(msg.text) 194 } 195 onSupportedSuffixesChanged: { 196 if (!pageLoader.item) 197 return 198 pageLoader.item.supportedFormats = supportedSuffixes 199 } 200 onEngineChanged: { // assume a engine canRender is only used as a renderer 201 subtitleItem.visible = canRender 202 subtitleLabel.visible = !canRender 203 } 204 onEnabledChanged: { 205 subtitleItem.visible = enabled 206 subtitleLabel.visible = enabled 207 } 208 } 209 210 MultiPointTouchArea { 211 //mouseEnabled: true //not available on qt5.2(ubuntu14.04) 212 anchors.fill: parent 213 onGestureStarted: { 214 if (player.playbackState == MediaPlayer.StoppedState) 215 return 216 var p = gesture.touchPoints[0] 217 var dx = p.x - p.previousX 218 var dy = p.y - p.previousY 219 var t = dy/dx 220 var ml = Math.abs(dx) + Math.abs(dy) 221 var ML = Math.abs(p.x - p.startX) + Math.abs(p.y - p.startY) 222 //console.log("dx: " + dx + " dy: " + dy + " ml: " + ml + " ML: " + ML) 223 if (ml < 2.0 || 5*ml < ML) 224 return 225 if (t > -1 && t < 1) { 226 player.fastSeek = true 227 if (dx > 0) { 228 player.seekForward() 229 } else { 230 player.seekBackward() 231 } 232 } 233 } 234 MouseArea { 235 anchors.fill: parent 236 hoverEnabled: true 237 cursorShape: control.opacity > 0 || cursor_timer.running ? Qt.ArrowCursor : Qt.BlankCursor 238 onWheel: { 239 var deg = wheel.angleDelta.y/8 240 var dp = wheel.pixelDelta 241 var p = Qt.point(mouseX, mouseY) //root.mapToItem(videoOut, Qt.point(mouseX, mouseY)) 242 var fp = videoOut.mapPointToSource(p) 243 if (fp.x < 0) 244 fp.x = 0; 245 if (fp.y < 0) 246 fp.y = 0; 247 if (fp.x > videoOut.videoFrameSize.width) 248 fp.x = videoOut.videoFrameSize.width 249 if (fp.y > videoOut.videoFrameSize.height) 250 fp.y = videoOut.videoFrameSize.height 251 videoOut.zoom *= (1.0 + deg*3.14/180.0); 252 if (videoOut.zoom < 1.0) 253 videoOut.zoom = 1.0 254 var x0 = fp.x - fp.x/videoOut.zoom; 255 var y0 = fp.y - fp.y/videoOut.zoom; 256 // in fact, it must insected with video frame rect. opengl save us 257 videoOut.regionOfInterest.x = x0 258 videoOut.regionOfInterest.y = y0 259 videoOut.regionOfInterest.width = videoOut.videoFrameSize.width/videoOut.zoom 260 videoOut.regionOfInterest.height = videoOut.videoFrameSize.height/videoOut.zoom 261 } 262 263 onDoubleClicked: { 264 control.toggleVisible() 265 } 266 onClicked: { 267 if (playList.state === "show") 268 return 269 if (playList.state === "hide") 270 playList.state = "ready" 271 else 272 playList.state = "hide" 273 274 } 275 276 onMouseXChanged: { 277 cursor_timer.start() 278 if (mouseX > root.width || mouseX < 0 279 || mouseY > root.height || mouseY < 0) 280 return; 281 if (mouseX < Utils.scaled(20)) { 282 if (playList.state === "hide") { 283 playList.state = "ready" 284 } 285 } 286 287 if (root.width - mouseX < configPanel.width) { //qt5.6 mouseX is very large if mouse released 288 //console.log("configPanel show: root width: " + root.width + " mouseX: " + mouseX + "panel width: " + configPanel.width) 289 configPanel.state = "show" 290 } else { 291 configPanel.state = "hide" 292 //console.log("configPanel hide: root width: " + root.width + " mouseX: " + mouseX + "panel width: " + configPanel.width) 293 } 294 if (player.playbackState == MediaPlayer.StoppedState || !player.hasVideo) 295 return; 296 if (mouseY < control.y - control.previewHeight) { 297 control.hidePreview() // TODO: check previw hovered too 298 } else { 299 if (pressed) { 300 control.showPreview(mouseX/parent.width) 301 } 302 } 303 } 304 Timer { 305 id: cursor_timer 306 interval: 2000 307 } 308 } 309 } 310 Text { 311 id: msg 312 objectName: "msg" 313 horizontalAlignment: Text.AlignHCenter 314 font.pixelSize: Utils.scaled(20) 315 style: Text.Outline 316 styleColor: "green" 317 color: "white" 318 anchors.top: root.top 319 width: root.width 320 height: root.height / 4 321 onTextChanged: { 322 msg_timer.stop() 323 visible = true 324 msg_timer.start() 325 } 326 Timer { 327 id: msg_timer 328 interval: 2000 329 onTriggered: msg.visible = false 330 } 331 function error(txt) { 332 styleColor = "red" 333 text = txt 334 } 335 function info(txt) { 336 styleColor = "green" 337 text = txt 338 } 339 } 340 341 Item { 342 anchors.fill: parent 343 focus: true 344 Keys.onPressed: { 345 switch (event.key) { 346 case Qt.Key_M: 347 control.mute = !control.mute 348 break 349 case Qt.Key_Right: 350 player.fastSeek = event.isAutoRepeat 351 player.seek(player.position + 10000) 352 break 353 case Qt.Key_Left: 354 player.fastSeek = event.isAutoRepeat 355 player.seek(player.position - 10000) 356 break 357 case Qt.Key_Up: 358 control.volume = Math.min(2, control.volume+0.05) 359 break 360 case Qt.Key_Down: 361 control.volume = Math.max(0, control.volume-0.05) 362 break 363 case Qt.Key_Space: 364 if (player.playbackState == MediaPlayer.PlayingState) { 365 player.pause() 366 } else if (player.playbackState == MediaPlayer.PausedState){ 367 player.play() 368 } 369 break 370 case Qt.Key_Plus: 371 player.playbackRate += 0.1; 372 console.log("player.playbackRate: " + player.playbackRate); 373 break; 374 case Qt.Key_Minus: 375 player.playbackRate = Math.max(0.1, player.playbackRate - 0.1); 376 break; 377 case Qt.Key_F: 378 control.toggleFullScreen() 379 break 380 case Qt.Key_R: 381 videoOut.orientation += 90 382 break; 383 case Qt.Key_T: 384 videoOut.orientation -= 90 385 break; 386 case Qt.Key_C: 387 player.videoCapture.capture() 388 break 389 case Qt.Key_A: 390 if (videoOut.fillMode === VideoOutput.Stretch) { 391 videoOut.fillMode = VideoOutput.PreserveAspectFit 392 } else if (videoOut.fillMode === VideoOutput.PreserveAspectFit) { 393 videoOut.fillMode = VideoOutput.PreserveAspectCrop 394 } else { 395 videoOut.fillMode = VideoOutput.Stretch 396 } 397 break 398 case Qt.Key_O: 399 fileDialog.open() 400 break; 401 case Qt.Key_N: 402 player.stepForward() 403 break 404 case Qt.Key_B: 405 player.stepBackward() 406 break; 407 //case Qt.Key_Back: 408 case Qt.Key_Q: 409 Qt.quit() 410 break 411 } 412 } 413 } 414 DropArea { 415 anchors.fill: root 416 onEntered: { 417 if (!drag.hasUrls) 418 return; 419 console.log(drag.urls) 420 player.source = drag.urls[0] 421 } 422 } 423 424 Item { 425 id: configPage 426 anchors.right: configPanel.left 427 anchors.rightMargin: -configPanel.anchors.rightMargin*Utils.scaled(20)/configPanel.width 428 //anchors.bottom: control.top 429 y: Math.max(0, Math.min(configPanel.selectedY, root.height - pageLoader.height - control.height)) 430 width: parent.width < 4*configPanel.width ? parent.width - configPanel.width : parent.width/2 + configPanel.width -16 431 // height: maxHeight 432 readonly property real maxHeight: control.y //- Math.max(0, configPanel.selectedY) 433 Loader { 434 id: pageLoader 435 anchors.right: parent.right 436 width: parent.width 437 focus: true 438 onLoaded: { 439 if (!item) 440 return 441 item.maxHeight = configPage.maxHeight 442 if (item.information) { 443 item.information = { 444 source: player.source, 445 hasAudio: player.hasAudio, 446 hasVideo: player.hasVideo, 447 metaData: player.metaData 448 } 449 } 450 if (item.hasOwnProperty("internalAudioTracks")) 451 item.internalAudioTracks = player.internalAudioTracks 452 if (typeof(item.externalAudioTracks) != "undefined") 453 item.externalAudioTracks = player.externalAudioTracks 454 if ("internalSubtitleTracks" in item) 455 item.internalSubtitleTracks = player.internalSubtitleTracks 456 } 457 } 458 Connections { 459 target: pageLoader.item 460 onVisibleChanged: { 461 if (!pageLoader.item.visible) 462 pageLoader.source = "" 463 } 464 onChannelChanged: player.channelLayout = channel 465 onExternalAudioChanged: player.externalAudio = file 466 onAudioTrackChanged: player.audioTrack = track 467 onSubtitleTrackChanged: player.internalSubtitleTrack = track 468 onBrightnessChanged: videoOut.brightness = target.brightness 469 onContrastChanged: videoOut.contrast = target.contrast 470 onHueChanged: videoOut.hue = target.hue 471 onSaturationChanged: { 472 console.log("saturation: " + target.saturation) 473 videoOut.saturation = target.saturation 474 } 475 } 476 } 477 ConfigPanel { 478 id: configPanel 479 anchors { 480 top: parent.top 481 right: parent.right 482 bottom: control.top 483 } 484 width: Utils.scaled(100) 485 onClicked: { 486 pageLoader.source = selectedUrl 487 if (pageLoader.item) 488 pageLoader.item.visible = true 489 } 490 onSelectedUrlChanged: pageLoader.source = selectedUrl 491 } 492 493 PlayListPanel { 494 id: playList 495 visible: Qt.platform.os !== "winrt" 496 anchors { 497 top: parent.top 498 left: parent.left 499 bottom: control.top 500 } 501 width: Math.min(parent.width, Utils.scaled(480)) - Utils.scaled(20) 502 Connections { 503 target: player 504 // onStatusChanged: too late to call status is wrong value 505 onDurationChanged: { 506 if (player.duration <= 0) 507 return 508 var url = player.source.toString() 509 if (url.startsWith("winrt:@")) { 510 url = url.substring(url.indexOf(":", 7) + 1); 511 } 512 console.log("duration changed: " + url) 513 playList.addHistory(url, player.duration) 514 } 515 } 516 onPlay: { 517 player.source = source 518 if (start > 0) 519 player.seek(start) 520 } 521 } 522 523 ControlPanel { 524 id: control 525 anchors { 526 left: parent.left 527 bottom: parent.bottom 528 right: parent.right 529 margins: Utils.scaled(12) 530 } 531 mediaSource: player.source 532 duration: player.duration 533 534 onSeek: { 535 player.fastSeek = false 536 player.seek(ms) 537 } 538 onSeekForward: { 539 player.fastSeek = false 540 player.seek(player.position + ms) 541 } 542 onSeekBackward: { 543 player.fastSeek = false 544 player.seek(player.position - ms) 545 } 546 onPlay: player.play() 547 onStop: player.stop() 548 onTogglePause: { 549 if (player.playbackState == MediaPlayer.PlayingState) { 550 player.pause() 551 } else { 552 player.play() 553 } 554 } 555 volume: player.volume 556 onOpenFile: fileDialog.open() 557 //IF_QT53 558 onOpenUrl: urlDialog.open() 559 //ENDIF_QT53 560 onShowInfo: pageLoader.source = "MediaInfoPage.qml" 561 onShowHelp: pageLoader.source = "About.qml" 562 } 563//IF_QT53 564 Dialog { 565 id: urlDialog 566 standardButtons: StandardButton.Open | StandardButton.Cancel 567 title: qsTr("Open a URL") 568 Rectangle { 569 color: "black" 570 anchors.top: parent.top 571 height: Utils.kItemHeight 572 width: parent.width 573 TextInput { 574 id: urlEdit 575 color: "orange" 576 font.pixelSize: Utils.kFontSize 577 anchors.fill: parent 578 } 579 } 580 onAccepted: player.source = urlEdit.displayText 581 } 582//ENDIF_QT53 583 FileDialog { 584 id: fileDialog 585 title: "Please choose a media file" 586 selectMultiple: true 587 folder: PlayerConfig.lastFile 588 onAccepted: { 589 var sub, av 590 for (var i = 0; i < fileUrls.length; ++i) { 591 var s = fileUrls[i].toString() 592 if (s.endsWith(".srt") 593 || s.endsWith(".ass") 594 || s.endsWith(".ssa") 595 || s.endsWith(".sub") 596 || s.endsWith(".idx") //vob 597 || s.endsWith(".mpl2") 598 || s.endsWith(".smi") 599 || s.endsWith(".sami") 600 || s.endsWith(".sup") 601 || s.endsWith(".txt")) 602 sub = fileUrls[i] 603 else 604 av = fileUrls[i] 605 } 606 if (sub) { 607 subtitle.autoLoad = false 608 subtitle.file = sub 609 } else { 610 subtitle.autoLoad = PlayerConfig.subtitleAutoLoad 611 subtitle.file = "" 612 } 613 if (av) { 614 player.source = av 615 PlayerConfig.lastFile = av 616 } 617 } 618 } 619 Connections { 620 target: Qt.application 621 onStateChanged: { //since 5.1 622 if (Qt.platform.os === "winrt" || Qt.platform.os === "winphone") //paused by system 623 return 624 // winrt is handled by system 625 switch (Qt.application.state) { 626 case Qt.ApplicationSuspended: 627 case Qt.ApplicationHidden: 628 player.pause() 629 break 630 default: 631 break 632 } 633 } 634 } 635 Connections { 636 target: PlayerConfig 637 onZeroCopyChanged: { 638 var opt = player.videoCodecOptions 639 if (PlayerConfig.zeroCopy) { 640 opt["copyMode"] = "ZeroCopy" 641 } else { 642 opt["copyMode"] = "OptimizedCopy" //FIXME: CUDA 643 } 644 player.videoCodecOptions = opt 645 } 646 } 647} 648