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