on xenial help sign in
Added live video playback in dash previews
commit9a8c144c52ffe4a84c01a5909d658db54f988369 authorAlbert Astals Cid <albert.astals@canonical.com> dateMon 27 Jun 2016, 18:41 (10 years ago) parente76202525d309a
29 files changed+2311−289
qml/Components/LazyImage.qml+3−1 qml/Components/MediaServices/MediaServices.qml+386 qml/Components/MediaServices/MediaServicesControls.qml+192 qml/Components/MediaServices/MediaServicesHeader.qml+71 qml/Components/MediaServices/VideoPlayer.qml+155 qml/Components/MediaServices/VideoPlayerControls.qml+158 qml/Dash/Previews/PreviewSharing.qml → qml/Components/SharingPicker.qml+2−14 qml/Dash/Previews/Preview.qml+16 qml/Dash/Previews/PreviewImageGallery.qml+2 qml/Dash/Previews/PreviewInlineVideo.qml+65 qml/Dash/Previews/PreviewMediaToolbar.qml+25−5 qml/Dash/Previews/PreviewWidget.qml+3 qml/Dash/Previews/PreviewWidgetFactory.qml+11−1 qml/Dash/Previews/PreviewZoomableImage.qml+1 tests/mocks/QtMultimedia/CMakeLists.txt+8−2 tests/mocks/QtMultimedia/VideoSurface.qml+111 tests/mocks/QtMultimedia/audio.cpp−132 tests/mocks/QtMultimedia/audio.h−103 tests/mocks/QtMultimedia/mediaplayer.cpp+318 tests/mocks/QtMultimedia/mediaplayer.h+223 tests/mocks/QtMultimedia/plugin.cpp+7−3 tests/mocks/QtMultimedia/videooutput.cpp+127 tests/mocks/QtMultimedia/videooutput.h+52 tests/qmltests/CMakeLists.txt+3−1 tests/qmltests/Components/tst_MediaServices.qml+230 tests/qmltests/Dash/Previews/tst_PreviewSharing.qml → tests/qmltests/Components/tst_SharingPicker.qml+8−8 tests/qmltests/Dash/Previews/tst_PreviewInlineVideo.qml+114 tests/qmltests/Dash/Previews/tst_PreviewMediaToolbar.qml+3−3 tests/qmltests/Dash/Previews/tst_PreviewWidgetFactory.qml+17−16
qml/Components/LazyImage.qml+3−1
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013-2016 Canonical, Ltd.
33 *
44 * Authors:
55 * Michał Sawicz <michal.sawicz@canonical.com>
@@ -29,6 +29,7 @@ Item {
2929
3030 property real initialWidth: scaleTo == "width" || scaleTo == "fit" ? width : units.gu(10)
3131 property real initialHeight: scaleTo == "height" || scaleTo == "fit" ? height : units.gu(10)
32+ property real lastScaledDimension: scaleTo == "height" || scaleTo == "fit" ? width : height
3233
3334 property alias sourceSize: image.sourceSize
3435 property alias asynchronous: image.asynchronous
@@ -168,6 +169,7 @@ Item {
168169 easing.type: Easing.Linear; duration: UbuntuAnimation.SnapDuration
169170 }
170171 }
172+ ScriptAction { script: { lastScaledDimension = scaleTo == "height" || scaleTo == "fit" ? root.width : root.height } }
171173 }
172174 },
173175
qml/Components/MediaServices/MediaServices.qmlnew file+386
@@ -0,0 +1,386 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtMultimedia 5.0
19+import Ubuntu.Components 1.3
20+
21+FocusScope {
22+ id: root
23+ property var sourceData
24+ property string context: ""
25+ property list<Action> actions
26+ property Item rootItem: QuickUtils.rootItem(root)
27+ property var maximumEmbeddedHeight
28+
29+ property alias header: headerContent.item
30+ property alias content: contentLoader.item
31+ property alias footer: footerContent.item
32+ property alias fullscreen: priv.fullscreen
33+
34+ signal close();
35+
36+ onFullscreenChanged: {
37+ if (!fullscreen) rotationAction.checked = false;
38+ }
39+
40+ readonly property color backgroundColor: "#1B1B1B"
41+ readonly property color iconColor: "#F3F3E7"
42+
43+ Rectangle {
44+ anchors.fill: parent
45+ color: root.backgroundColor
46+ opacity: 0.85
47+ }
48+
49+ OrientationHelper {
50+ id: orientationHelper
51+ automaticOrientation: fullscreen
52+
53+ StateGroup {
54+ id: orientationState
55+
56+ states: [
57+ State {
58+ name: "portrait"
59+ when: !rotationAction.checked
60+ },
61+ State {
62+ name: "landscape"
63+ when: rotationAction.checked
64+ }
65+ ]
66+
67+ transitions: [
68+ Transition {
69+ to: "landscape"
70+ SequentialAnimation {
71+ PropertyAction { target: orientationHelper; property: "automaticOrientation"; value: false }
72+ PropertyAction { target: orientationHelper; property: "orientationAngle"; value: 90 }
73+ PropertyAction { target: orientationHelper; property: "automaticOrientation"; value: false }
74+ }
75+ },
76+ Transition {
77+ to: "portrait"
78+ SequentialAnimation {
79+ PropertyAction { target: orientationHelper; property: "automaticOrientation"; value: false }
80+ PropertyAction { target: orientationHelper; property: "orientationAngle"; value: 0 }
81+ PropertyAction { target: orientationHelper; property: "automaticOrientation"; value: fullscreen }
82+ }
83+ }
84+ ]
85+ }
86+
87+ Loader {
88+ id: contentLoader
89+
90+ sourceComponent: {
91+ switch (context) {
92+ case "video":
93+ return videoComponent;
94+ }
95+ return undefined;
96+ }
97+
98+ Component {
99+ id: videoComponent
100+ VideoPlayer {
101+ id: player
102+ objectName: "videoPlayer"
103+
104+ width: orientationHelper.width
105+ height: orientationHelper.height
106+ maximumEmbeddedHeight: root.maximumEmbeddedHeight
107+ fixedHeight: fullscreen
108+ orientation: orientationState.state === "landscape" ? Qt.LandscapeOrientation : Qt.PortraitOrientation
109+
110+ playButtonBackgroundColor: root.backgroundColor
111+ playButtonIconColor: root.iconColor
112+
113+ screenshot: {
114+ var screenshotData = root.sourceData["screenshot"];
115+ if (screenshotData) return screenshotData;
116+
117+ var source = root.sourceData["source"];
118+ if (source) {
119+ if (source.toString().indexOf("file://") === 0) {
120+ return "image://thumbnailer/" + source.toString().substr(7);
121+ }
122+ }
123+ return "";
124+ }
125+
126+ mediaPlayer: footer ? footer.mediaPlayer : null
127+
128+ onClicked: {
129+ if (mediaPlayer.availability !== MediaPlayer.Available) return;
130+
131+ if (mediaPlayer.playbackState === MediaPlayer.StoppedState) {
132+ mediaPlayer.play();
133+ } else if (controlHideTimer.running) {
134+ if (mediaPlayer.playbackState === MediaPlayer.PlayingState) {
135+ mediaPlayer.pause();
136+ } else {
137+ mediaPlayer.play();
138+ }
139+ } else {
140+ controlHideTimer.restart();
141+ }
142+ }
143+ onPositionChanged: controlHideTimer.restart();
144+ }
145+ }
146+ }
147+ }
148+
149+ Loader {
150+ id: headerContent
151+ anchors {
152+ top: parent.top
153+ left: parent.left
154+ right: parent.right
155+ topMargin: -headerContent.height
156+ }
157+ height: units.gu(6)
158+ visible: false
159+
160+ // eater
161+ MouseArea {
162+ anchors.fill: parent
163+ }
164+
165+ Rectangle {
166+ anchors.fill: parent
167+ color: root.backgroundColor
168+ opacity: 0.85
169+ visible: headerContent.status === Loader.Ready
170+ }
171+
172+ active: root.fullscreen
173+ sourceComponent: headerComponent
174+
175+ Component {
176+ id: headerComponent
177+
178+ MediaServicesHeader {
179+ iconColor: root.iconColor
180+ onGoPrevious: {
181+ rotationAction.checked = false;
182+ root.close();
183+ }
184+ }
185+ }
186+
187+ // If we interact with the bar, reset the hide timer.
188+ MouseArea {
189+ anchors.fill: parent
190+ onPressed: {
191+ mouse.accepted = false
192+ if (controlHideTimer.running) controlHideTimer.restart()
193+ }
194+ }
195+ }
196+
197+ Loader {
198+ id: footerContent
199+ anchors {
200+ left: parent.left
201+ right: parent.right
202+ bottom: parent.bottom
203+ bottomMargin: -footerContent.height
204+ }
205+ height: units.gu(7)
206+ visible: false
207+
208+ sourceComponent: {
209+ switch (context) {
210+ case "video":
211+ return videoControlsComponent;
212+ }
213+ return undefined;
214+ }
215+ // eater
216+ MouseArea {
217+ anchors.fill: parent
218+ }
219+
220+ Rectangle {
221+ anchors.fill: parent
222+ color: root.backgroundColor
223+ opacity: 0.85
224+ visible: footerContent.status === Loader.Ready
225+ }
226+
227+ Component {
228+ id: videoControlsComponent
229+ VideoPlayerControls {
230+ id: controls
231+ objectName: "videoControls"
232+
233+ viewAction: rotationAction.enabled ? rotationAction : fullscreenAction
234+ userActions: root.actions
235+ iconColor: root.iconColor
236+ backgroundColor: root.backgroundColor
237+
238+ mediaPlayer.source: {
239+ if (!root.sourceData) return "";
240+
241+ var source = root.sourceData["source"];
242+ if (source.toString().indexOf("video://") === 0) {
243+ return source.toString().substr(6);
244+ }
245+ return source;
246+ }
247+ mediaPlayer.onPlaybackStateChanged: {
248+ controlHideTimer.restart();
249+ }
250+
251+ Binding {
252+ target: priv
253+ property: "forceControlsShown"
254+
255+ value: (fullscreen && mediaPlayer.playbackState === MediaPlayer.StoppedState) ||
256+ mediaPlayer.playbackState === MediaPlayer.PausedState ||
257+ interacting
258+ }
259+
260+ onInteractingChanged: {
261+ controlHideTimer.restart();
262+ }
263+
264+ Binding {
265+ target: header
266+ property: "title"
267+ value: controls.mediaPlayer.metaData.title !== undefined ?
268+ controls.mediaPlayer.metaData.title :
269+ controls.mediaPlayer.source.toString().replace(/^.*[\\\/]/, '')
270+ when: header != null
271+ }
272+ }
273+ }
274+
275+ // If we interact with the bar, reset the hide timer.
276+ MouseArea {
277+ z: 1
278+ anchors.fill: parent
279+ onPressed: {
280+ mouse.accepted = false
281+ if (controlHideTimer.running) controlHideTimer.restart()
282+ }
283+ }
284+ }
285+
286+ StateGroup {
287+ states: [
288+ State {
289+ name: "controlsShown"
290+ when: priv.forceControlsShown || priv.controlTimerActive
291+ PropertyChanges {
292+ target: footerContent
293+ anchors.bottomMargin: 0
294+ visible: true
295+ }
296+ PropertyChanges {
297+ target: headerContent
298+ anchors.topMargin: 0
299+ visible: true
300+ }
301+ }
302+ ]
303+
304+ transitions: [
305+ Transition {
306+ to: "controlsShown"
307+ SequentialAnimation {
308+ PropertyAction { target: root; property: "clip"; value: true }
309+ PropertyAction { property: "visible" }
310+ NumberAnimation {
311+ properties: "anchors.bottomMargin,anchors.topMargin"
312+ duration: UbuntuAnimation.FastDuration
313+ }
314+ PropertyAction { target: root; property: "clip"; value: false }
315+ }
316+ },
317+ Transition {
318+ from: "controlsShown"
319+ SequentialAnimation {
320+ PropertyAction { target: root; property: "clip"; value: true }
321+ NumberAnimation {
322+ properties: "anchors.bottomMargin,anchors.topMargin"
323+ duration: UbuntuAnimation.FastDuration
324+ }
325+ PropertyAction { property: "visible" }
326+ PropertyAction { target: root; property: "clip"; value: false }
327+ }
328+ }
329+ ]
330+ }
331+
332+ StateGroup {
333+ states: [
334+ State {
335+ name: "minimized"
336+ when: !priv.fullscreen
337+ PropertyChanges { target: root; implicitHeight: content ? content.implicitHeight : 0; }
338+ },
339+ State {
340+ name: "fullscreen"
341+ when: priv.fullscreen
342+ ParentChange { target: root; parent: rootItem; x: 0; y: 0; width: parent.width; }
343+ PropertyChanges { target: root; implicitHeight: root.parent ? root.parent.height : 0; }
344+ PropertyChanges { target: fullscreenAction; iconName: "view-restore"; }
345+ }
346+ ]
347+
348+ transitions: Transition {
349+ ParentAnimation {
350+ UbuntuNumberAnimation { properties: "x,y,width,implicitHeight"; duration: UbuntuAnimation.FastDuration }
351+ }
352+ }
353+ }
354+
355+ QtObject {
356+ id: priv
357+
358+ property bool fullscreen: false
359+ property alias controlTimerActive: controlHideTimer.running
360+ property bool forceControlsShown: false
361+ }
362+
363+ Timer {
364+ id: controlHideTimer
365+ objectName: "controlHideTimer"
366+ interval: 4000
367+ running: false
368+ }
369+
370+ Action {
371+ id: fullscreenAction
372+ enabled: rotationAction.enabled === false
373+ iconName: "view-fullscreen"
374+
375+ onTriggered: priv.fullscreen = !priv.fullscreen
376+ }
377+
378+ Action {
379+ id: rotationAction
380+ enabled: root.fullscreen === true
381+ iconName: "view-rotate"
382+
383+ property bool checked: false
384+ onTriggered: checked = !checked
385+ }
386+}
qml/Components/MediaServices/MediaServicesControls.qmlnew file+192
@@ -0,0 +1,192 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtQuick.Layouts 1.1
19+import QtMultimedia 5.0
20+import Ubuntu.Components 1.3
21+import Ubuntu.Components.ListItems 1.3 as ListItem
22+
23+Item {
24+ id: root
25+
26+ property alias component: loader.sourceComponent
27+
28+ signal actionClicked(string action)
29+ signal viewModeClicked
30+
31+ property list<Action> userActions
32+ property Action viewAction
33+
34+ property color iconColor
35+ property color backgroundColor
36+
37+ RowLayout {
38+ anchors {
39+ left: parent.left
40+ right: parent.right
41+ verticalCenter: parent.verticalCenter
42+ margins: units.gu(2)
43+ }
44+ spacing: units.gu(2)
45+
46+ AbstractButton {
47+ id: actionButton
48+ Layout.preferredWidth: units.gu(3)
49+ Layout.preferredHeight: units.gu(3)
50+ Layout.alignment: Qt.AlignVCenter
51+ enabled: action && action.enabled
52+
53+ Action {
54+ id: popupAction
55+ iconName: "navigation-menu"
56+ onTriggered: userActionsPopup.createObject(root, { "anchors.bottom": root.top })
57+ }
58+
59+ action: {
60+ switch (userActions.length) {
61+ case 0:
62+ return null;
63+ case 1:
64+ return userActions[0];
65+ default:
66+ return popupAction;
67+ }
68+ }
69+
70+ Icon {
71+ anchors.fill: parent
72+ visible: actionButton.action && actionButton.action.iconSource !== "" || false
73+ source: actionButton.action ? actionButton.action.iconSource : ""
74+ color: root.iconColor
75+ opacity: actionButton.action && actionButton.action.enabled ? 1.0 : 0.5
76+ }
77+ }
78+
79+ Loader {
80+ id: loader
81+ Layout.fillWidth: true
82+ Layout.preferredHeight: units.gu(3)
83+ }
84+
85+ AbstractButton {
86+ objectName: "viewActionButton"
87+ Layout.preferredWidth: units.gu(3)
88+ Layout.preferredHeight: units.gu(3)
89+ Layout.alignment: Qt.AlignVCenter
90+ enabled: viewAction.enabled
91+ action: viewAction
92+
93+ Icon {
94+ anchors.fill: parent
95+ visible: viewAction.iconSource !== ""
96+ source: viewAction.iconSource
97+ color: root.iconColor
98+ opacity: viewAction.enabled ? 1.0 : 0.5
99+ }
100+ }
101+ }
102+
103+ Component {
104+ id: userActionsPopup
105+
106+ Rectangle {
107+ id: popup
108+ color: root.backgroundColor
109+ width: userActionsColumn.width
110+ height: userActionsColumn.height
111+
112+ InverseMouseArea {
113+ id: eventGrabber
114+ acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
115+ anchors.fill: popup
116+ propagateComposedEvents: false
117+ onWheel: wheel.accepted = true
118+
119+ onPressed: popup.destroy()
120+ }
121+
122+ Column {
123+ id: userActionsColumn
124+ spacing: units.gu(1)
125+ width: units.gu(31)
126+
127+ Repeater {
128+ id: actionRepeater
129+ model: userActions
130+ AbstractButton {
131+ action: modelData
132+
133+ onClicked: popup.destroy()
134+
135+ implicitHeight: units.gu(4) + bottomDividerLine.height
136+ width: parent ? parent.width : units.gu(31)
137+
138+ Rectangle {
139+ visible: parent.pressed
140+ anchors {
141+ left: parent.left
142+ right: parent.right
143+ top: parent.top
144+ }
145+ height: parent.height - bottomDividerLine.height
146+ opacity: 0.5
147+ }
148+
149+ Icon {
150+ id: actionIcon
151+ visible: "" !== action.iconSource
152+ source: action.iconSource
153+ color: root.iconColor
154+ anchors {
155+ verticalCenter: parent.verticalCenter
156+ verticalCenterOffset: units.dp(-1)
157+ left: parent.left
158+ leftMargin: units.gu(2)
159+ }
160+ width: units.gu(2)
161+ height: units.gu(2)
162+ opacity: action.enabled ? 1.0 : 0.5
163+ }
164+
165+ Label {
166+ anchors {
167+ verticalCenter: parent.verticalCenter
168+ verticalCenterOffset: units.dp(-1)
169+ left: actionIcon.visible ? actionIcon.right : parent.left
170+ leftMargin: units.gu(2)
171+ right: parent.right
172+ }
173+ // In the tabs overflow panel there are no icons, and the font-size
174+ // is medium as opposed to the small font-size in the actions overflow panel.
175+ fontSize: actionIcon.visible ? "small" : "medium"
176+ elide: Text.ElideRight
177+ text: action.text
178+ color: root.iconColor
179+ opacity: action.enabled ? 1.0 : 0.5
180+ }
181+
182+ ListItem.ThinDivider {
183+ id: bottomDividerLine
184+ anchors.bottom: parent.bottom
185+ visible: index !== actionRepeater.count - 1
186+ }
187+ }
188+ }
189+ }
190+ }
191+ }
192+}
qml/Components/MediaServices/MediaServicesHeader.qmlnew file+71
@@ -0,0 +1,71 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtQuick.Layouts 1.1
19+import Ubuntu.Components 1.3
20+
21+Item {
22+ id: root
23+
24+ property alias title: titleLabel.text
25+ property alias component: loader.sourceComponent
26+ property alias iconColor: titleLabel.color
27+
28+ signal goPrevious
29+
30+ RowLayout {
31+ id: row
32+ anchors {
33+ left: parent.left
34+ right: parent.right
35+ verticalCenter: parent.verticalCenter
36+ margins: units.gu(2)
37+ }
38+ spacing: units.gu(2)
39+
40+ AbstractButton {
41+ id: navigationButton
42+ objectName: "navigationButton"
43+ Layout.preferredWidth: units.gu(3)
44+ Layout.preferredHeight: units.gu(3)
45+ Layout.alignment: Qt.AlignVCenter
46+
47+ Icon {
48+ anchors.fill: parent
49+ name: "go-previous"
50+ color: titleLabel.color
51+ }
52+
53+ onTriggered: {
54+ root.goPrevious();
55+ }
56+ }
57+
58+ Label {
59+ id: titleLabel
60+ Layout.fillWidth: true
61+ Layout.alignment: Qt.AlignVCenter
62+ elide: Text.ElideRight
63+ }
64+
65+ Loader {
66+ id: loader
67+ Layout.alignment: Qt.AlignVCenter
68+ Layout.preferredHeight: units.gu(3)
69+ }
70+ }
71+}
qml/Components/MediaServices/VideoPlayer.qmlnew file+155
@@ -0,0 +1,155 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtMultimedia 5.0
19+import Ubuntu.Components 1.3
20+import Ubuntu.Thumbnailer 0.1
21+import "../../Components"
22+
23+Item {
24+ id: root
25+
26+ property alias screenshot: image.source
27+ property alias mediaPlayer: videoOutput.source
28+ property int orientation: Qt.PortraitOrientation
29+ property bool fixedHeight: false
30+ property var maximumEmbeddedHeight
31+
32+ property alias playButtonBackgroundColor: playButton.color
33+ property alias playButtonIconColor: playButtonIcon.color
34+
35+ implicitHeight: {
36+ if (parent && orientation === Qt.LandscapeOrientation) {
37+ return parent.height;
38+ }
39+ return content.height;
40+ }
41+
42+ signal clicked
43+ signal positionChanged
44+
45+ Item {
46+ id: content
47+ anchors {
48+ left: parent.left
49+ right: parent.right
50+ verticalCenter: parent.verticalCenter
51+ }
52+ height: {
53+ if (root.orientation === Qt.LandscapeOrientation || fixedHeight) {
54+ return root.height;
55+ }
56+ var proposedHeight = videoOutput.height;
57+ if (maximumEmbeddedHeight !== undefined && maximumEmbeddedHeight < proposedHeight) {
58+ return maximumEmbeddedHeight;
59+ }
60+ return proposedHeight;
61+ }
62+ clip: image.height > videoOutput.height
63+
64+ LazyImage {
65+ id: image
66+ objectName: "screenshot"
67+ anchors {
68+ left: parent.left
69+ right: parent.right
70+ verticalCenter: parent.verticalCenter
71+ }
72+ scaleTo: "width"
73+ lastScaledDimension: playButton.height + units.gu(2)
74+ initialHeight: lastScaledDimension
75+ useUbuntuShape: false
76+
77+ visible: !mediaPlayer || mediaPlayer.playbackState === MediaPlayer.StoppedState
78+ }
79+
80+ VideoOutput {
81+ id: videoOutput
82+ anchors.centerIn: parent
83+
84+ width: root.width
85+ height: {
86+ if (fixedHeight) {
87+ return root.height;
88+ }
89+ var proposedHeight = mediaPlayer && mediaPlayer.metaData.resolution !== undefined ?
90+ (mediaPlayer.metaData.resolution.height / mediaPlayer.metaData.resolution.width) * width :
91+ image.height;
92+ if (maximumEmbeddedHeight !== undefined && maximumEmbeddedHeight < proposedHeight) {
93+ return maximumEmbeddedHeight;
94+ }
95+ return proposedHeight;
96+ }
97+
98+ source: mediaPlayer
99+ visible: mediaPlayer && mediaPlayer.playbackState !== MediaPlayer.StoppedState || false
100+
101+ Connections {
102+ target: mediaPlayer
103+ onError: {
104+ if (error !== MediaPlayer.NoError) {
105+ errorTimer.restart();
106+ }
107+ }
108+ }
109+ }
110+ }
111+
112+ Rectangle {
113+ id: playButton
114+ readonly property bool bigButton: parent.width > units.gu(40)
115+ anchors.centerIn: content
116+ width: bigButton ? units.gu(10) : units.gu(8)
117+ height: width
118+ visible: mediaPlayer && mediaPlayer.playbackState !== MediaPlayer.PlayingState || false
119+ opacity: 0.85
120+ radius: width/2
121+
122+ Behavior on width { UbuntuNumberAnimation {} }
123+
124+ Icon {
125+ id: playButtonIcon
126+ anchors.fill: parent
127+ anchors.margins: units.gu(1)
128+ name: errorTimer.running ? "dialog-warning-symbolic" : "media-playback-start"
129+ }
130+ }
131+
132+ ActivityIndicator {
133+ anchors.centerIn: content
134+ running: {
135+ if (!mediaPlayer) return false;
136+ return mediaPlayer.status === MediaPlayer.Stalled ||
137+ (mediaPlayer.playbackState === MediaPlayer.PlayingState && mediaPlayer.status === MediaPlayer.Loading);
138+ }
139+ }
140+
141+ MouseArea {
142+ id: contentMouseArea
143+ anchors.fill: content
144+ enabled: !errorTimer.running
145+ hoverEnabled: mediaPlayer && mediaPlayer.playbackState !== MediaPlayer.StoppedState || false
146+
147+ onClicked: root.clicked()
148+ onPositionChanged: root.positionChanged()
149+ }
150+
151+ Timer {
152+ id: errorTimer
153+ interval: 2000
154+ }
155+}
qml/Components/MediaServices/VideoPlayerControls.qmlnew file+158
@@ -0,0 +1,158 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtQuick.Layouts 1.1
19+import QtMultimedia 5.0
20+import Ubuntu.Components 1.3
21+
22+MediaServicesControls {
23+ id: root
24+ readonly property alias mediaPlayer: _mediaPlayer
25+ property bool interacting: false
26+
27+ QtObject {
28+ id: priv
29+
30+ function formatProgress(time) {
31+ time = Math.floor(time / 1000);
32+
33+ var secs = time % 60;
34+ time = Math.floor(time / 60);
35+ var min = time % 60;
36+ var hour = Math.floor(time / 60);
37+
38+ if (secs < 10) secs = "0%1".arg(secs);
39+ if (min < 10) min = "0%1".arg(min);
40+ if (hour > 0) {
41+ // TRANSLATORS: this refers to a duration/remaining time of the video in hours, minutes and seconds,
42+ // of which you can change the order.
43+ // %1 refers to hours, %2 refers to minutes and %3 refers to seconds.
44+ return i18n.tr("%1:%2:%3").arg(hour).arg(min).arg(secs);
45+ } else {
46+ // TRANSLATORS: this refers to a duration/remaining time of the video in minutes and seconds,
47+ // of which you can change the order.
48+ // %1 refers to minutes and %2 refers to seconds.
49+ return i18n.tr("%1:%2").arg(min).arg(secs);
50+ }
51+ }
52+ }
53+
54+ component: Item {
55+ Connections {
56+ target: mediaPlayer
57+ onPositionChanged: {
58+ if (slider.valueGuard) return;
59+
60+ slider.valueGuard = true;
61+ slider.value = mediaPlayer.position;
62+ slider.valueGuard = false;
63+ if (!slider.pressed) {
64+ positionLabel.text = priv.formatProgress(mediaPlayer.position);
65+ }
66+ }
67+ }
68+
69+ Binding {
70+ target: root
71+ property: "interacting"
72+ value: slider.pressed
73+ }
74+
75+ Label {
76+ id: positionLabel
77+ anchors {
78+ left: parent.left
79+ bottom: parent.bottom
80+ bottomMargin: -units.dp(3)
81+ }
82+ verticalAlignment: Text.AlignBottom
83+ fontSize: "x-small"
84+ color: root.iconColor
85+
86+ text: priv.formatProgress(mediaPlayer.position)
87+ }
88+
89+ Slider {
90+ id: slider
91+ property bool valueGuard: false
92+
93+ anchors {
94+ left: parent.left
95+ right: parent.right
96+ }
97+ height: units.gu(2)
98+ live: true
99+ enabled: mediaPlayer.seekable && mediaPlayer.duration > 0
100+ minimumValue: 0
101+ maximumValue: mediaPlayer.duration > 0 ? mediaPlayer.duration : 1
102+ value: mediaPlayer.position
103+
104+ onStyleInstanceChanged: {
105+ if (__styleInstance) __styleInstance.backgroundColor = root.iconColor;
106+ }
107+
108+ onValueChanged: {
109+ if (!pressed || slider.valueGuard) return;
110+
111+ slider.valueGuard = true;
112+ mediaPlayer.seek(value);
113+ slider.valueGuard = false;
114+ }
115+
116+ property bool wasPlaying: mediaPlayer.playbackState === MediaPlayer.PlayingState
117+ onPressedChanged: {
118+ if (pressed) {
119+ wasPlaying = mediaPlayer.playbackState === MediaPlayer.PlayingState
120+ mediaPlayer.pause();
121+ } else {
122+ positionLabel.text = priv.formatProgress(mediaPlayer.position);
123+ if (wasPlaying) {
124+ mediaPlayer.play();
125+ }
126+ }
127+ }
128+
129+ function formatValue(value) {
130+ return priv.formatProgress(value);
131+ }
132+ }
133+
134+ Label {
135+ anchors {
136+ right: parent.right
137+ bottom: parent.bottom
138+ bottomMargin: -units.dp(3)
139+ }
140+ verticalAlignment: Text.AlignBottom
141+ fontSize: "x-small"
142+ color: root.iconColor
143+
144+ text: priv.formatProgress(mediaPlayer.duration)
145+ }
146+ }
147+
148+ MediaPlayer {
149+ id: _mediaPlayer
150+ objectName: "mediaPlayer"
151+
152+ onError: {
153+ if (error !== MediaPlayer.NoError) {
154+ stop();
155+ }
156+ }
157+ }
158+}
qml/Dash/Previews/PreviewSharing.qml → qml/Components/SharingPicker.qmlrenamed+2−14
@@ -20,8 +20,6 @@ import Ubuntu.Content 1.1
2020
2121Item {
2222 id: root
23− implicitHeight: button.height
24− implicitWidth: button.width
2523
2624 property var shareData
2725 property alias active: peerPicker.active
@@ -30,18 +28,8 @@ Item {
3028 readonly property var url: shareData ? shareData["uri"] : ""
3129 readonly property Item rootItem: QuickUtils.rootItem(root)
3230
33− AbstractButton {
34− id: button
35− height: units.gu(4)
36− width: units.gu(4)
37− onClicked: peerPicker.visible = true
38−
39− Icon {
40− anchors.centerIn: parent
41− height: units.gu(3)
42− width: units.gu(3)
43− source: "image://theme/share"
44− }
31+ function showPeerPicker() {
32+ peerPicker.visible = true;
4533 }
4634
4735 function createExportedItems(url) {
qml/Dash/Previews/Preview.qml+16
@@ -37,12 +37,16 @@ Item {
3737 //! \brief The ScopeStyle component.
3838 property var scopeStyle: null
3939
40+ //! Should the orientation be locked
41+ property int orientationLockCount: 0
42+
4043 clip: true
4144
4245 Binding {
4346 target: previewModel
4447 property: "widgetColumnCount"
4548 value: row.columns
49+ when: root.orientationLockCount === 0
4650 }
4751
4852 MouseArea {
@@ -110,6 +114,18 @@ Item {
110114 column.forceLayout();
111115 column.positionViewAtIndex(index, ListView.Contain)
112116 }
117+
118+ onOrientationLockChanged: {
119+ if (orientationLock)
120+ root.orientationLockCount++;
121+ else
122+ root.orientationLockCount = Math.max(0, root.orientationLockCount--);
123+ }
124+
125+ Component.onDestruction: {
126+ if (orientationLock)
127+ root.orientationLockCount = Math.max(0, root.orientationLockCount--);
128+ }
113129 }
114130 }
115131 }
qml/Dash/Previews/PreviewImageGallery.qml+2
@@ -27,6 +27,8 @@ PreviewWidget {
2727 id: root
2828 implicitHeight: units.gu(22)
2929
30+ orientationLock: overlay.visible
31+
3032 property Item rootItem: QuickUtils.rootItem(root)
3133
3234 ListView {
qml/Dash/Previews/PreviewInlineVideo.qmlnew file+65
@@ -0,0 +1,65 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtMultimedia 5.0
19+import Ubuntu.Components 1.3
20+import Ubuntu.Thumbnailer 0.1
21+import "../../Components"
22+import "../../Components/MediaServices"
23+
24+/*! \brief Preview widget for video.
25+
26+ This widget shows video contained in widgetData["source"],
27+ with a placeholder screenshot specified by widgetData["screenshot"].
28+ */
29+
30+PreviewWidget {
31+ id: root
32+ implicitWidth: units.gu(35)
33+ implicitHeight: services.height
34+
35+ widgetMargins: -units.gu(1)
36+ orientationLock: services.fullscreen
37+
38+ property alias rootItem: services.rootItem
39+
40+ MediaServices {
41+ id: services
42+ width: parent.width
43+
44+ actions: sharingAction
45+ context: "video"
46+ sourceData: widgetData
47+ fullscreen: false
48+ maximumEmbeddedHeight: rootItem.height / 2
49+
50+ onClose: fullscreen = false
51+
52+ Action {
53+ id: sharingAction
54+ iconName: "share"
55+ visible: sharingPicker.active
56+ onTriggered: sharingPicker.showPeerPicker()
57+ }
58+ }
59+
60+ SharingPicker {
61+ id: sharingPicker
62+ objectName: "sharingPicker"
63+ shareData: widgetData["share-data"]
64+ }
65+}
qml/Dash/Previews/PreviewMediaToolbar.qml+25−5
@@ -19,19 +19,39 @@ import Ubuntu.Components 1.3
1919import "../../Components"
2020
2121Rectangle {
22− property alias shareData: sharingWidget.shareData
22+ property alias shareData: sharingPicker.shareData
2323
2424 implicitHeight: units.gu(6)
2525 color: Qt.rgba(0, 0, 0, 0.8)
26− visible: sharingWidget.active
26+ visible: sharingPicker.active
2727
28− PreviewSharing {
29− id: sharingWidget
30− objectName: "sharingWidget"
28+ Action {
29+ id: sharingAction
30+ iconName: "share"
31+ onTriggered: sharingPicker.showPeerPicker()
32+ }
33+
34+ AbstractButton {
35+ id: button
3136 anchors {
3237 left: parent.left
3338 verticalCenter: parent.verticalCenter
3439 leftMargin: units.gu(1)
3540 }
41+ height: units.gu(4)
42+ width: units.gu(4)
43+ action: sharingAction
44+
45+ Icon {
46+ anchors.centerIn: parent
47+ height: units.gu(3)
48+ width: units.gu(3)
49+ source: button.action.iconSource
50+ }
51+ }
52+
53+ SharingPicker {
54+ id: sharingPicker
55+ objectName: "sharingPicker"
3656 }
3757}
qml/Dash/Previews/PreviewWidget.qml+3
@@ -31,6 +31,9 @@ Item {
3131 //! Should the widget show in expanded mode (For those that support it)
3232 property bool expanded: true
3333
34+ //! Should the orientation be locked
35+ property bool orientationLock: false
36+
3437 //! Set margins width.
3538 property real widgetMargins: units.gu(1)
3639
qml/Dash/Previews/PreviewWidgetFactory.qml+11−1
@@ -37,6 +37,9 @@ Loader {
3737 //! Should the widget show in expanded mode (For those that support it)
3838 property bool expanded: widgetType !== "expandable" || widgetData["expanded"] === true
3939
40+ //! Should the orientation be locked
41+ readonly property bool orientationLock: status === Loader.Ready ? item.orientationLock : false
42+
4043 //! Set margins width.
4144 property real widgetMargins: status === Loader.Ready ? item.widgetMargins : units.gu(1)
4245
@@ -70,7 +73,14 @@ Loader {
7073 case "reviews": return "PreviewRatingDisplay.qml";
7174 case "table": return "PreviewTable.qml";
7275 case "text": return "PreviewTextSummary.qml";
73− case "video": return "PreviewVideoPlayback.qml";
76+ case "video": {
77+ if (!widgetData) return "";
78+ var source = widgetData.hasOwnProperty("source") ? widgetData["source"].toString() : "";
79+ if (source.match("^https{0,1}\:") !== null) {
80+ return "PreviewVideoPlayback.qml";
81+ }
82+ return "PreviewInlineVideo.qml";
83+ }
7484 default: return "";
7585 }
7686 }
qml/Dash/Previews/PreviewZoomableImage.qml+1
@@ -31,6 +31,7 @@ PreviewWidget {
3131 implicitHeight: lazyImage.height
3232
3333 widgetMargins: -units.gu(1)
34+ orientationLock: overlay.visible
3435
3536 property Item rootItem: QuickUtils.rootItem(root)
3637
tests/mocks/QtMultimedia/CMakeLists.txt+8−2
@@ -1,9 +1,15 @@
1+include_directories(
2+ ${CMAKE_CURRENT_SOURCE_DIR}
3+ ${CMAKE_CURRENT_BINARY_DIR}
4+)
5+
16add_library(QtMultimedia-qml MODULE
27 plugin.cpp
3− audio.cpp
8+ mediaplayer.cpp
9+ videooutput.cpp
410 declarativeplaylist.cpp
511 )
612
7−qt5_use_modules(QtMultimedia-qml Qml)
13+qt5_use_modules(QtMultimedia-qml Qml Quick)
814
915add_unity8_mock(QtMultimedia 5.0 QtMultimedia TARGETS QtMultimedia-qml)
tests/mocks/QtMultimedia/VideoSurface.qmlnew file+111
@@ -0,0 +1,111 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import Ubuntu.Components 1.3
19+import QtMultimedia 5.0
20+
21+Rectangle {
22+ id: root
23+ property int position: 0
24+ property int playbackState: 0
25+ property var resolution: { "width": 16, "height": 9 }
26+
27+ anchors.centerIn: parent
28+ color: "black"
29+
30+ // scale to the parent window.
31+ implicitWidth: parent ? aspectScale(Qt.size(resolution.width, resolution.height), Qt.size(parent.width, parent.height)).width : 160
32+ implicitHeight: parent ? aspectScale(Qt.size(resolution.width, resolution.height), Qt.size(parent.width, parent.height)).height : 90
33+
34+ function aspectScale(fromSize, toSize) {
35+ var rw = toSize.height * fromSize.width / fromSize.height;
36+ var useHeight = (rw <= toSize.width);
37+
38+ if (useHeight) {
39+ return Qt.size(rw, toSize.height);
40+ } else {
41+ return Qt.size(toSize.width,
42+ toSize.width * (fromSize.height / fromSize.width));
43+ }
44+ }
45+
46+ GridView {
47+ anchors.fill: parent
48+ model: (parent.width / units.gu(5)) * ((parent.height / units.gu(5) + 1))
49+ clip: true
50+
51+ cellHeight: units.gu(5)
52+ cellWidth: units.gu(5)
53+
54+ delegate: Item {
55+ width: units.gu(5)
56+ height: units.gu(5)
57+
58+ Rectangle {
59+ id: rect
60+ color: "red"
61+ anchors.fill: parent
62+ anchors.margins: units.gu(1)
63+
64+ Connections {
65+ target: root
66+ onPositionChanged: {
67+ rect.rotation = position/100
68+ }
69+ }
70+ }
71+ }
72+ }
73+
74+ Column {
75+ anchors {
76+ left: parent.left;
77+ right: parent.right
78+ verticalCenter: parent.verticalCenter
79+ }
80+ height: units.gu(10)
81+
82+ Label {
83+ anchors.horizontalCenter: parent.horizontalCenter
84+ text: {
85+ var pos = (position/1000).toFixed(0)
86+
87+ var m = Math.floor(pos/60);
88+ var ss = pos % 60;
89+ if (ss >= 10) {
90+ return m + ":" + ss;
91+ } else {
92+ return m + ":0" + ss;
93+ }
94+ }
95+ fontSize: "x-large"
96+ }
97+
98+ Label {
99+ anchors.horizontalCenter: parent.horizontalCenter
100+ text: getPlaybackState(playbackState)
101+ fontSize: "x-large"
102+ }
103+ }
104+
105+ function getPlaybackState(playbackState) {
106+ if (playbackState === MediaPlayer.PlayingState) return "Playing";
107+ else if (playbackState === MediaPlayer.PausedState) return "Paused";
108+ else if (playbackState === MediaPlayer.StoppedState) return "Stopped";
109+ return "";
110+ }
111+}
tests/mocks/QtMultimedia/audio.cppdeleted−132
@@ -1,132 +0,0 @@
1−/*
2− * Copyright (C) 2013-2016 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− *
16− * Author: Michael Zanetti <michael.zanetti@canonical.com>
17− */
18−
19−#include "audio.h"
20−
21−Audio::Audio(QObject* parent):
22− QObject(parent),
23− m_playbackState(StoppedState),
24− m_playlist(nullptr)
25−{
26− qsrand(time(nullptr));
27− m_timer.setInterval(1000);
28− connect(&m_timer, &QTimer::timeout, this, &Audio::timerEvent);
29−
30− m_position = 0;
31− Q_EMIT positionChanged(m_position);
32−
33− m_duration = (qrand() % 20000) + 10000;
34− Q_EMIT durationChanged(m_duration);
35−}
36−
37−QUrl Audio::source() const
38−{
39− return m_source;
40−}
41−
42−void Audio::setSource(const QUrl &source)
43−{
44− if (m_source != source) {
45− m_source = source;
46− Q_EMIT sourceChanged(source);
47− }
48−}
49−
50−Audio::PlaybackState Audio::playbackState() const
51−{
52− return m_playbackState;
53−}
54−
55−int Audio::position() const
56−{
57− return m_position;
58−}
59−
60−int Audio::duration() const
61−{
62− return m_duration;
63−}
64−
65−QString Audio::errorString() const
66−{
67− return QString();
68−}
69−
70−void Audio::pause()
71−{
72− if (m_playbackState == PlayingState) {
73− m_playbackState = PausedState;
74− Q_EMIT playbackStateChanged(m_playbackState);
75− m_timer.stop();
76− }
77−}
78−
79−void Audio::play()
80−{
81− if (m_playbackState != PlayingState) {
82− m_playbackState = PlayingState;
83− Q_EMIT playbackStateChanged(m_playbackState);
84−
85− m_timer.start();
86− }
87−}
88−
89−void Audio::stop()
90−{
91− if (m_playbackState != StoppedState) {
92− m_playbackState = StoppedState;
93− Q_EMIT playbackStateChanged(m_playbackState);
94− m_timer.stop();
95− m_position = 0;
96− Q_EMIT positionChanged(m_position);
97− }
98−}
99−
100−void Audio::timerEvent()
101−{
102− if (m_position + 1000 < m_duration) {
103− m_position += 1000;
104− Q_EMIT positionChanged(m_position);
105− } else {
106− stop();
107− }
108−}
109−
110−Audio::AudioRole Audio::audioRole() const
111−{
112− return Audio::MusicRole;
113−}
114−
115−void Audio::setAudioRole(Audio::AudioRole audioRole)
116−{
117− Q_UNUSED(audioRole);
118−}
119−
120−DeclarativePlaylist *Audio::playlist() const
121−{
122− return m_playlist;
123−}
124−
125−void Audio::setPlaylist(DeclarativePlaylist *playlist)
126−{
127− if (playlist == m_playlist)
128− return;
129−
130− m_playlist = playlist;
131− Q_EMIT playlistChanged();
132−}
tests/mocks/QtMultimedia/audio.hdeleted−103
@@ -1,103 +0,0 @@
1−/*
2− * Copyright (C) 2012-2016 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− *
16− */
17−
18−#ifndef MOCK_AUDIO_H
19−#define MOCK_AUDIO_H
20−
21−#include <QObject>
22−#include <QUrl>
23−#include <QTimer>
24−
25−class DeclarativePlaylist;
26−
27−class Audio: public QObject
28−{
29− Q_OBJECT
30− Q_ENUMS(PlaybackState)
31− Q_ENUMS(AudioRole)
32− Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged)
33− Q_PROPERTY(PlaybackState playbackState READ playbackState NOTIFY playbackStateChanged)
34− Q_PROPERTY(int position READ position NOTIFY positionChanged)
35− Q_PROPERTY(int duration READ duration NOTIFY durationChanged)
36− Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged)
37− Q_PROPERTY(AudioRole audioRole READ audioRole WRITE setAudioRole)
38− Q_PROPERTY(DeclarativePlaylist *playlist READ playlist WRITE setPlaylist NOTIFY playlistChanged)
39−public:
40− enum PlaybackState {
41− PlayingState,
42− PausedState,
43− StoppedState
44− };
45−
46− enum AudioRole {
47− UnknownRole,
48− MusicRole,
49− VideoRole,
50− VoiceCommunicationRole,
51− AlarmRole,
52− NotificationRole,
53− RingtoneRole,
54− AccessibilityRole,
55− SonificationRole,
56− GameRole
57− };
58−
59− explicit Audio(QObject *parent = nullptr);
60−
61− QUrl source() const;
62− void setSource(const QUrl &source);
63−
64− DeclarativePlaylist *playlist() const;
65− void setPlaylist(DeclarativePlaylist *playlist);
66−
67− PlaybackState playbackState() const;
68−
69− int position() const;
70−
71− int duration() const;
72−
73− QString errorString() const;
74−
75− AudioRole audioRole() const;
76− void setAudioRole(AudioRole audioRole);
77−
78−public Q_SLOTS:
79− void pause();
80− void play();
81− void stop();
82−
83−Q_SIGNALS:
84− void playlistChanged();
85− void sourceChanged(const QUrl &source);
86− void playbackStateChanged(PlaybackState playbackState);
87− void positionChanged(int position);
88− void durationChanged(int duration);
89− void errorStringChanged(const QString &errorString);
90−
91−private Q_SLOTS:
92− void timerEvent();
93−
94−private:
95− QUrl m_source;
96− PlaybackState m_playbackState;
97− QTimer m_timer;
98− int m_position;
99− int m_duration;
100− DeclarativePlaylist *m_playlist;
101−};
102−
103−#endif
tests/mocks/QtMultimedia/mediaplayer.cppnew file+318
@@ -0,0 +1,318 @@
1+/*
2+ * Copyright (C) 2013-2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ *
16+ * Author: Michael Zanetti <michael.zanetti@canonical.com>
17+ */
18+
19+#include "mediaplayer.h"
20+
21+#include <QDebug>
22+#include <QSize>
23+
24+class MetaDataObject: public QObject
25+{
26+ Q_OBJECT
27+ Q_PROPERTY(QVariant title READ title NOTIFY metaDataChanged)
28+ Q_PROPERTY(QVariant resolution READ resolution NOTIFY metaDataChanged)
29+public:
30+ MetaDataObject(MediaPlayer* parent = nullptr)
31+ : QObject(parent)
32+ , m_source(parent)
33+ {
34+ }
35+
36+ QVariant title() const { return getProperty("title"); }
37+ QVariant resolution() const { return getProperty("resolution", QSize(640, 640)); }
38+
39+ QVariant getProperty(const QString& property, QVariant defaultValue = QVariant()) const {
40+ MediaDataSource* sourceData = MediaPlayerDataController::instance()->dataForSource(m_source->source());
41+ QVariant metaData = sourceData->metaData();
42+ return metaData.toMap().value(property, defaultValue);
43+ }
44+
45+Q_SIGNALS:
46+ void metaDataChanged();
47+
48+private:
49+ MediaPlayer* m_source;
50+};
51+
52+MediaPlayer::MediaPlayer(QObject* parent)
53+ : QObject(parent)
54+ , m_playbackState(StoppedState)
55+ , m_status(NoMedia)
56+ , m_metaData(new MetaDataObject(this))
57+ , m_playlist(nullptr)
58+{
59+ qsrand(time(nullptr));
60+ m_timer.setInterval(100);
61+ connect(&m_timer, &QTimer::timeout, this, &MediaPlayer::timerEvent);
62+
63+ connect(MediaPlayerDataController::instance(), &MediaPlayerDataController::sourceAboutToBeRemoved,
64+ this, [this](const QUrl& source) {
65+ MediaDataSource* dataSource = MediaPlayerDataController::instance()->dataForSource(source);
66+ if (!dataSource) return;
67+
68+ disconnect(dataSource, &MediaDataSource::durationChanged, this, &MediaPlayer::durationChanged);
69+ disconnect(dataSource, &MediaDataSource::seekableChanged, this, &MediaPlayer::seekableChanged);
70+ disconnect(dataSource, &MediaDataSource::availabilityChanged, this, &MediaPlayer::availabilityChanged);
71+ disconnect(dataSource, &MediaDataSource::metaDataChanged, m_metaData, &MetaDataObject::metaDataChanged);
72+ });
73+
74+ connect(MediaPlayerDataController::instance(), &MediaPlayerDataController::sourceAdded,
75+ this, [this](const QUrl& source) {
76+ MediaDataSource* dataSource = MediaPlayerDataController::instance()->dataForSource(source);
77+ if (!dataSource) return;
78+
79+ connect(dataSource, &MediaDataSource::durationChanged, this, &MediaPlayer::durationChanged);
80+ connect(dataSource, &MediaDataSource::seekableChanged,this, &MediaPlayer::seekableChanged);
81+ connect(dataSource, &MediaDataSource::availabilityChanged,this, &MediaPlayer::availabilityChanged);
82+ connect(dataSource, &MediaDataSource::metaDataChanged, m_metaData, &MetaDataObject::metaDataChanged);
83+ });
84+}
85+
86+QUrl MediaPlayer::source() const
87+{
88+ return m_source;
89+}
90+
91+void MediaPlayer::setSource(const QUrl &source)
92+{
93+ if (m_source != source) {
94+ m_source = source;
95+ Q_EMIT sourceChanged(source);
96+ Q_EMIT durationChanged(duration());
97+ Q_EMIT seekableChanged(isSeekable());
98+ Q_EMIT availabilityChanged(availability());
99+
100+ m_position = 0;
101+ Q_EMIT positionChanged(m_position);
102+ Q_EMIT m_metaData->metaDataChanged();
103+
104+ m_status = availability()==Available ? Loaded : InvalidMedia;
105+ Q_EMIT statusChanged();
106+ }
107+}
108+
109+DeclarativePlaylist *MediaPlayer::playlist() const
110+{
111+ return m_playlist;
112+}
113+
114+void MediaPlayer::setPlaylist(DeclarativePlaylist *playlist)
115+{
116+ if (playlist == m_playlist)
117+ return;
118+
119+ m_playlist = playlist;
120+ Q_EMIT playlistChanged();
121+}
122+
123+MediaPlayer::PlaybackState MediaPlayer::playbackState() const
124+{
125+ return m_playbackState;
126+}
127+
128+int MediaPlayer::position() const
129+{
130+ return m_position;
131+}
132+
133+int MediaPlayer::duration() const
134+{
135+ MediaDataSource* dataSource = MediaPlayerDataController::instance()->dataForSource(source());
136+ if (dataSource) return dataSource->duration();
137+ return 0;
138+}
139+
140+MediaPlayer::Error MediaPlayer::error() const
141+{
142+ return NoError;
143+}
144+
145+QString MediaPlayer::errorString() const
146+{
147+ return QString();
148+}
149+
150+void MediaPlayer::pause()
151+{
152+ if (m_playbackState == PlayingState) {
153+ m_playbackState = PausedState;
154+ Q_EMIT playbackStateChanged(m_playbackState);
155+ m_timer.stop();
156+ }
157+}
158+
159+void MediaPlayer::play()
160+{
161+ if (m_playbackState != PlayingState) {
162+ m_playbackState = PlayingState;
163+ Q_EMIT playbackStateChanged(m_playbackState);
164+
165+ m_timer.start();
166+ }
167+}
168+
169+void MediaPlayer::stop()
170+{
171+ if (m_playbackState != StoppedState) {
172+ m_playbackState = StoppedState;
173+ Q_EMIT playbackStateChanged(m_playbackState);
174+ m_timer.stop();
175+ m_position = 0;
176+ Q_EMIT positionChanged(m_position);
177+ }
178+}
179+
180+void MediaPlayer::seek(int position)
181+{
182+ if (status() != Loaded) return;
183+ int newPosition = qMin(qMax(0, position), duration());
184+ if (newPosition != m_position) {
185+ m_position = newPosition;
186+ Q_EMIT positionChanged(m_position);
187+ }
188+}
189+
190+void MediaPlayer::timerEvent()
191+{
192+ if (m_position + m_timer.interval() < duration()) {
193+ m_position += m_timer.interval();
194+ Q_EMIT positionChanged(m_position);
195+ } else {
196+ pause();
197+ }
198+}
199+
200+MediaPlayer::AudioRole MediaPlayer::audioRole() const
201+{
202+ return MediaPlayer::MusicRole;
203+}
204+
205+void MediaPlayer::setAudioRole(MediaPlayer::AudioRole audioRole)
206+{
207+ Q_UNUSED(audioRole);
208+}
209+
210+bool MediaPlayer::isSeekable() const
211+{
212+ MediaDataSource* dataSource = MediaPlayerDataController::instance()->dataForSource(source());
213+ if (dataSource) return dataSource->isSeekable();
214+ return true;
215+}
216+
217+MediaPlayer::Availability MediaPlayer::availability() const
218+{
219+ MediaDataSource* dataSource = MediaPlayerDataController::instance()->dataForSource(source());
220+ if (dataSource) return dataSource->availability();
221+ return Available;
222+}
223+
224+QObject *MediaPlayer::metaData() const
225+{
226+ return m_metaData;
227+}
228+
229+
230+MediaDataSource::MediaDataSource(QObject *parent)
231+ : QObject(parent)
232+ , m_seekable(true)
233+ , m_duration((qrand() % 20000) + 10000)
234+ , m_availability(MediaPlayer::Available)
235+{
236+}
237+
238+MediaDataSource::~MediaDataSource()
239+{
240+ if (!m_source.isEmpty()) MediaPlayerDataController::instance()->unregisterDataSource(this);
241+}
242+
243+void MediaDataSource::setSource(const QUrl &source)
244+{
245+ if (m_source != source) {
246+ if (!m_source.isEmpty()) MediaPlayerDataController::instance()->unregisterDataSource(this);
247+
248+ m_source = source;
249+
250+ if (!m_source.isEmpty()) MediaPlayerDataController::instance()->registerDataSource(this);
251+ Q_EMIT sourceChanged();
252+ }
253+}
254+
255+void MediaDataSource::setSeekable(bool seekable)
256+{
257+ if (m_seekable != seekable) {
258+ m_seekable = seekable;
259+ Q_EMIT seekableChanged(m_seekable);
260+ }
261+}
262+
263+void MediaDataSource::setDuration(int duration)
264+{
265+ if (m_duration != duration) {
266+ m_duration = duration;
267+ Q_EMIT durationChanged(m_duration);
268+ }
269+}
270+
271+void MediaDataSource::setAvailability(MediaPlayer::Availability availability)
272+{
273+ if (m_availability != availability) {
274+ m_availability = availability;
275+ Q_EMIT availabilityChanged(m_availability);
276+ }
277+}
278+
279+void MediaDataSource::setMetaData(const QVariant& metaData)
280+{
281+ if (m_metaData != metaData) {
282+ m_metaData = metaData;
283+ Q_EMIT metaDataChanged();
284+ }
285+}
286+
287+MediaPlayerDataController *MediaPlayerDataController::instance()
288+{
289+ static MediaPlayerDataController* instance = nullptr;
290+ if (!instance) { instance = new MediaPlayerDataController(); }
291+ return instance;
292+}
293+
294+void MediaPlayerDataController::registerDataSource(MediaDataSource *dataSource)
295+{
296+ m_dataSources[dataSource->source()] = dataSource;
297+ Q_EMIT sourceAdded(dataSource->source());
298+}
299+
300+void MediaPlayerDataController::unregisterDataSource(MediaDataSource *dataSource)
301+{
302+ QList<QUrl> keys = m_dataSources.keys(dataSource);
303+ Q_FOREACH(const QUrl& key, keys) {
304+ Q_EMIT sourceAboutToBeRemoved(key);
305+ m_dataSources.remove(key);
306+ }
307+}
308+
309+MediaDataSource *MediaPlayerDataController::dataForSource(const QUrl &source)
310+{
311+ if (!m_dataSources.contains(source)) {
312+ static MediaDataSource defaultSource;
313+ return &defaultSource;
314+ }
315+ return m_dataSources[source];
316+}
317+
318+#include "mediaplayer.moc"
tests/mocks/QtMultimedia/mediaplayer.hnew file+223
@@ -0,0 +1,223 @@
1+/*
2+ * Copyright (C) 2012-2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ *
16+ */
17+
18+#ifndef MOCK_MEDIAPLAYER_H
19+#define MOCK_MEDIAPLAYER_H
20+
21+#include <QObject>
22+#include <QUrl>
23+#include <QTimer>
24+#include <QHash>
25+#include <QVariant>
26+
27+class MetaDataObject;
28+class DeclarativePlaylist;
29+
30+class MediaPlayer: public QObject
31+{
32+ Q_OBJECT
33+
34+ Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged)
35+ Q_PROPERTY(PlaybackState playbackState READ playbackState NOTIFY playbackStateChanged)
36+ Q_PROPERTY(int position READ position NOTIFY positionChanged)
37+ Q_PROPERTY(int duration READ duration NOTIFY durationChanged)
38+ Q_PROPERTY(Error error READ error NOTIFY errorChanged)
39+ Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged)
40+ Q_PROPERTY(AudioRole audioRole READ audioRole WRITE setAudioRole)
41+
42+ Q_PROPERTY(bool seekable READ isSeekable NOTIFY seekableChanged)
43+ Q_PROPERTY(Availability availability READ availability NOTIFY availabilityChanged)
44+ Q_PROPERTY(Status status READ status NOTIFY statusChanged)
45+ Q_PROPERTY(QObject *metaData READ metaData CONSTANT)
46+ Q_PROPERTY(DeclarativePlaylist *playlist READ playlist WRITE setPlaylist NOTIFY playlistChanged)
47+
48+ Q_ENUMS(PlaybackState)
49+ Q_ENUMS(AudioRole)
50+ Q_ENUMS(Availability)
51+ Q_ENUMS(Status)
52+ Q_ENUMS(Error)
53+public:
54+ enum PlaybackState {
55+ PlayingState,
56+ PausedState,
57+ StoppedState
58+ };
59+
60+ enum AudioRole {
61+ UnknownRole,
62+ MusicRole,
63+ VideoRole,
64+ VoiceCommunicationRole,
65+ AlarmRole,
66+ NotificationRole,
67+ RingtoneRole,
68+ AccessibilityRole,
69+ SonificationRole,
70+ GameRole
71+ };
72+
73+ enum Availability {
74+ Available,
75+ Busy,
76+ Unavailable,
77+ ResourceMissing
78+ };
79+
80+ enum Status {
81+ UnknownStatus,
82+ NoMedia,
83+ Loading,
84+ Loaded,
85+ Stalled,
86+ Buffering,
87+ Buffered,
88+ EndOfMedia,
89+ InvalidMedia
90+ };
91+
92+ enum Error {
93+ NoError,
94+ ResourceError,
95+ FormatError,
96+ NetworkError,
97+ AccessDeniedError,
98+ ServiceMissingError
99+ };
100+
101+ explicit MediaPlayer(QObject *parent = nullptr);
102+
103+ QUrl source() const;
104+ void setSource(const QUrl &source);
105+
106+ DeclarativePlaylist *playlist() const;
107+ void setPlaylist(DeclarativePlaylist *playlist);
108+
109+ PlaybackState playbackState() const;
110+
111+ int position() const;
112+
113+ int duration() const;
114+
115+ Error error() const;
116+ QString errorString() const;
117+
118+ AudioRole audioRole() const;
119+ void setAudioRole(AudioRole audioRole);
120+
121+ bool isSeekable() const;
122+ MediaPlayer::Availability availability() const;
123+ Status status() const { return m_status; }
124+ QObject *metaData() const;
125+
126+public Q_SLOTS:
127+ void pause();
128+ void play();
129+ void stop();
130+ void seek(int position);
131+
132+Q_SIGNALS:
133+ void playlistChanged();
134+ void sourceChanged(const QUrl &source);
135+ void playbackStateChanged(PlaybackState playbackState);
136+ void positionChanged(int position);
137+ void durationChanged(int duration);
138+ void seekableChanged(bool seekable);
139+ void errorChanged(Error error);
140+ void errorStringChanged(const QString &errorString);
141+ void availabilityChanged(Availability availability);
142+ void statusChanged();
143+
144+ void error(Error error, const QString &errorString);
145+
146+private Q_SLOTS:
147+ void timerEvent();
148+
149+private:
150+ QUrl m_source;
151+ PlaybackState m_playbackState;
152+ QTimer m_timer;
153+ int m_position;
154+ Status m_status;
155+ MetaDataObject *m_metaData;
156+ DeclarativePlaylist *m_playlist;
157+};
158+
159+class MediaDataSource : public QObject
160+{
161+ Q_OBJECT
162+ Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged)
163+ Q_PROPERTY(bool seekable READ isSeekable WRITE setSeekable NOTIFY seekableChanged)
164+ Q_PROPERTY(int duration READ duration WRITE setDuration NOTIFY durationChanged)
165+ Q_PROPERTY(MediaPlayer::Availability availability READ availability WRITE setAvailability NOTIFY availabilityChanged)
166+ Q_PROPERTY(QVariant metaData READ metaData WRITE setMetaData NOTIFY metaDataChanged)
167+
168+public:
169+ MediaDataSource(QObject* parent = 0);
170+ ~MediaDataSource();
171+
172+ QUrl source() const { return m_source; }
173+ void setSource(const QUrl& source);
174+
175+ bool isSeekable() const { return m_seekable; }
176+ void setSeekable(bool seekable);
177+
178+ int duration() const { return m_duration; }
179+ void setDuration(int duration);
180+
181+ MediaPlayer::Availability availability() const { return m_availability; }
182+ void setAvailability(MediaPlayer::Availability availability);
183+
184+ QVariant metaData() const { return m_metaData; }
185+ void setMetaData(const QVariant& metaData);
186+
187+Q_SIGNALS:
188+ void sourceChanged();
189+ void seekableChanged(bool seekable);
190+ void durationChanged(int duration);
191+ void availabilityChanged(MediaPlayer::Availability availability);
192+ void metaDataChanged();
193+
194+private:
195+ QUrl m_source;
196+ bool m_seekable;
197+ int m_duration;
198+ MediaPlayer::Availability m_availability;
199+ QVariant m_metaData;
200+};
201+
202+class MediaPlayerDataController : public QObject
203+{
204+ Q_OBJECT
205+public:
206+ MediaPlayerDataController() = default;
207+
208+ static MediaPlayerDataController *instance();
209+
210+ void registerDataSource(MediaDataSource* dataSource);
211+ void unregisterDataSource(MediaDataSource* dataSource);
212+
213+ MediaDataSource* dataForSource(const QUrl& source);
214+
215+Q_SIGNALS:
216+ void sourceAdded(const QUrl& source);
217+ void sourceAboutToBeRemoved(const QUrl& source);
218+
219+public:
220+ QHash<QUrl, MediaDataSource*> m_dataSources;
221+};
222+
223+#endif // MOCK_MEDIAPLAYER_H
tests/mocks/QtMultimedia/plugin.cpp+7−3
@@ -17,15 +17,19 @@
1717 */
1818
1919#include "plugin.h"
20−#include "audio.h"
20+#include "mediaplayer.h"
2121#include "declarativeplaylist.h"
22+#include "videooutput.h"
2223
2324#include <QtQml/qqml.h>
2425
2526void MockQtMultimediaPlugin::registerTypes(const char *uri)
2627{
2728 Q_ASSERT(uri == QLatin1String("QtMultimedia"));
28− qmlRegisterType<Audio>(uri, 5, 0, "Audio");
29− qmlRegisterType<Audio>(uri, 5, 0, "MediaPlayer");
29+ qmlRegisterType<MediaPlayer>(uri, 5, 0, "Audio");
30+ qmlRegisterType<MediaPlayer>(uri, 5, 0, "MediaPlayer");
31+ qmlRegisterType<VideoOutput>(uri, 5, 0, "VideoOutput");
32+
33+ qmlRegisterType<MediaDataSource>(uri, 5, 0, "MediaDataSource");
3034 qmlRegisterType<DeclarativePlaylist>(uri, 5, 6, "Playlist");
3135}
tests/mocks/QtMultimedia/videooutput.cppnew file+127
@@ -0,0 +1,127 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "videooutput.h"
18+#include "mediaplayer.h"
19+
20+#include <paths.h>
21+
22+#include <QQuickView>
23+#include <QQmlComponent>
24+#include <QGuiApplication>
25+#include <QQmlProperty>
26+#include <QQmlEngine>
27+
28+VideoOutput::VideoOutput(QQuickItem *parent)
29+ : QQuickItem(parent)
30+ , m_source(nullptr)
31+ , m_qmlContentComponent(nullptr)
32+ , m_qmlItem(nullptr)
33+{
34+}
35+
36+void VideoOutput::setSource(QObject *source)
37+{
38+ if (m_source != source) {
39+ if (m_source) disconnect(m_source, 0, this, 0);
40+
41+ m_source = source;
42+ Q_EMIT sourceChanged();
43+
44+ MediaPlayer* mediaPlayer = qobject_cast<MediaPlayer*>(source);
45+ if (mediaPlayer) {
46+ connect(mediaPlayer, &MediaPlayer::positionChanged, this, &VideoOutput::updateProperties);
47+ connect(mediaPlayer, &MediaPlayer::playbackStateChanged, this, &VideoOutput::updateProperties);
48+ connect(mediaPlayer->metaData(), SIGNAL(metaDataChanged()), this, SLOT(updateProperties()));
49+ }
50+ }
51+}
52+
53+void VideoOutput::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value)
54+{
55+ if (change == QQuickItem::ItemSceneChange && !m_qmlContentComponent) {
56+ QWindowList list = QGuiApplication::topLevelWindows();
57+ if (list.isEmpty()) return;
58+
59+ // The assumptions I make here really should hold.
60+ QQuickView *quickView =
61+ qobject_cast<QQuickView*>(list[0]);
62+
63+ m_qmlContentComponent = new QQmlComponent(quickView->engine(),
64+ QString("%1/QtMultimedia/VideoSurface.qml").arg(mockPluginsDir()));
65+
66+ switch (m_qmlContentComponent->status()) {
67+ case QQmlComponent::Ready:
68+ createQmlContentItem();
69+ break;
70+ case QQmlComponent::Loading:
71+ connect(m_qmlContentComponent, &QQmlComponent::statusChanged,
72+ this, &VideoOutput::onComponentStatusChanged);
73+ break;
74+ case QQmlComponent::Error:
75+ printComponentErrors();
76+ qFatal("VideoOutput: failed to create content component.");
77+ break;
78+ default:
79+ qFatal("VideoOutput: Unhandled component status");
80+ }
81+ }
82+ QQuickItem::itemChange(change, value);
83+}
84+
85+void VideoOutput::printComponentErrors()
86+{
87+ QList<QQmlError> errors = m_qmlContentComponent->errors();
88+ for (int i = 0; i < errors.count(); ++i) {
89+ qDebug() << errors[i];
90+ }
91+}
92+
93+void VideoOutput::onComponentStatusChanged(QQmlComponent::Status status)
94+{
95+ if (status == QQmlComponent::Ready) {
96+ createQmlContentItem();
97+ }
98+}
99+
100+void VideoOutput::createQmlContentItem()
101+{
102+ m_qmlItem = qobject_cast<QQuickItem*>(m_qmlContentComponent->create());
103+ m_qmlItem->setParentItem(this);
104+
105+ updateProperties();
106+}
107+
108+void VideoOutput:: updateProperties()
109+{
110+ if (!m_qmlItem) return;
111+
112+ MediaPlayer* mediaPlayer = qobject_cast<MediaPlayer*>(m_source);
113+ if (!mediaPlayer) return;
114+
115+ QQmlProperty playbackStateProperty(m_qmlItem, "playbackState");
116+ playbackStateProperty.write(QVariant::fromValue<int>(mediaPlayer->playbackState()));
117+
118+ QQmlProperty positionProperty(m_qmlItem, "position");
119+ positionProperty.write(QVariant::fromValue(mediaPlayer->position()));
120+
121+ QQmlProperty propResSource(mediaPlayer->metaData(), "resolution");
122+ QQmlProperty propResDest(m_qmlItem, "resolution");
123+
124+ if (propResSource.isValid() && propResDest.isValid()) {
125+ propResDest.write(propResSource.read());
126+ }
127+}
tests/mocks/QtMultimedia/videooutput.hnew file+52
@@ -0,0 +1,52 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef VIDEOOUTPUT_H
18+#define VIDEOOUTPUT_H
19+
20+#include <QQuickItem>
21+#include <QPointer>
22+class QQmlComponent;
23+
24+class VideoOutput : public QQuickItem
25+{
26+ Q_OBJECT
27+ Q_PROPERTY(QObject* source READ source WRITE setSource NOTIFY sourceChanged)
28+public:
29+ explicit VideoOutput(QQuickItem *parent = 0);
30+
31+ QObject *source() const { return m_source.data(); }
32+ void setSource(QObject *source);
33+
34+ void itemChange(ItemChange change, const ItemChangeData & value);
35+
36+Q_SIGNALS:
37+ void sourceChanged();
38+
39+protected Q_SLOTS:
40+ void onComponentStatusChanged(QQmlComponent::Status status);
41+ void updateProperties();
42+
43+private:
44+ void createQmlContentItem();
45+ void printComponentErrors();
46+
47+ QPointer<QObject> m_source;
48+ QQmlComponent* m_qmlContentComponent;
49+ QQuickItem* m_qmlItem;
50+};
51+
52+#endif // VIDEOOUTPUT_H
tests/qmltests/CMakeLists.txt+3−1
@@ -12,11 +12,13 @@ add_unity8_qmltest(Components Dialogs LIGHTDM)
1212add_unity8_qmltest(Components DraggingArea)
1313add_unity8_qmltest(Components LazyImage)
1414add_unity8_qmltest(Components Lockscreen)
15+add_unity8_qmltest(Components MediaServices)
1516add_unity8_qmltest(Components ModeSwitchWarningDialog)
1617add_unity8_qmltest(Components PhysicalKeysMapper)
1718add_unity8_qmltest(Components Rating)
1819add_unity8_qmltest(Components ResponsiveGridView)
1920add_unity8_qmltest(Components ResponsiveVerticalJournal)
21+add_unity8_qmltest(Components SharingPicker)
2022add_unity8_qmltest(Components Showable)
2123add_unity8_qmltest(Components VirtualTouchPad)
2224add_unity8_qmltest(Components WallpaperResolver)
@@ -40,6 +42,7 @@ add_unity8_qmltest(Dash/Previews PreviewExpandable)
4042add_unity8_qmltest(Dash/Previews PreviewHeader)
4143add_unity8_qmltest(Dash/Previews PreviewIconActions)
4244add_unity8_qmltest(Dash/Previews PreviewImageGallery)
45+add_unity8_qmltest(Dash/Previews PreviewInlineVideo)
4346add_unity8_qmltest(Dash/Previews PreviewMediaToolbar)
4447add_unity8_qmltest(Dash/Previews PreviewPayments)
4548add_unity8_qmltest(Dash/Previews PreviewProgress)
@@ -47,7 +50,6 @@ add_unity8_qmltest(Dash/Previews PreviewRatingDisplay)
4750add_unity8_qmltest(Dash/Previews PreviewRatingDisplayCreationRanges)
4851add_unity8_qmltest(Dash/Previews PreviewRatingEdit)
4952add_unity8_qmltest(Dash/Previews PreviewRatingInput)
50−add_unity8_qmltest(Dash/Previews PreviewSharing)
5153add_unity8_qmltest(Dash/Previews PreviewTable)
5254add_unity8_qmltest(Dash/Previews PreviewTextSummary)
5355add_unity8_qmltest(Dash/Previews PreviewVideoPlayback)
tests/qmltests/Components/tst_MediaServices.qmlnew file+230
@@ -0,0 +1,230 @@
1+/*
2+ * Copyright 2016 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtTest 1.0
19+import "../../../qml/Components/MediaServices"
20+import Unity.Test 0.1 as UT
21+import Ubuntu.Components 1.3
22+import QtMultimedia 5.0
23+
24+Rectangle {
25+ id: root
26+ width: units.gu(70)
27+ height: units.gu(80)
28+ color: "lightgrey"
29+
30+ property var sourceData: {
31+ // TODO do we want a video or make this fake
32+ "source": "file:///home/nick/Videos/test-mpeg.ogv",
33+ "screenshot": Qt.resolvedUrl("../Dash/artwork/avatar.png")
34+ }
35+
36+ property var sourceData2: {
37+ // TODO do we want a video or make this fake
38+ "source": "file:///home/nick/Videos/test-mpeg2.ogv",
39+ "screenshot": Qt.resolvedUrl("../Dash/artwork/checkers.png")
40+ }
41+
42+ MediaDataSource {
43+ source: root.sourceData["source"]
44+ duration: 60000
45+ metaData: {
46+ "title" : "TEST MPEG",
47+ "resolution" : { "width": 160, "height": 90 }
48+ }
49+ }
50+
51+ MediaDataSource {
52+ source: root.sourceData2["source"]
53+ duration: 30000
54+ metaData: {
55+ "title" : "TEST MPEG",
56+ "resolution" : { "width": 90, "height": 240 }
57+ }
58+ }
59+
60+ Item {
61+ anchors.fill: parent
62+
63+ Rectangle {
64+ anchors {
65+ top: parent.top
66+ bottom: parent.bottom
67+ left: parent.left
68+ right: controls.left
69+ }
70+ color: "darkblue"
71+
72+ MediaServices {
73+ id: services
74+ fullscreen: false
75+ width: parent.width
76+ sourceData: root.sourceData
77+ context: "video"
78+
79+ rootItem: parent
80+ maximumEmbeddedHeight: undefined
81+ }
82+ }
83+
84+ Rectangle {
85+ id: controls
86+ color: "darkgrey"
87+ anchors {
88+ top: parent.top
89+ bottom: parent.bottom
90+ right: parent.right
91+ }
92+ width: units.gu(30)
93+
94+ Column {
95+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
96+
97+ Button {
98+ text: "Fullscreen"
99+ onClicked: services.fullscreen = !services.fullscreen
100+ }
101+ UT.MouseTouchEmulationCheckbox {}
102+ }
103+ }
104+ }
105+
106+ SignalSpy {
107+ id: serviceCloseSpy
108+ target: services
109+ signalName: "close"
110+ }
111+
112+ UT.UnityTestCase {
113+ name: "VideoMediaServices"
114+ when: windowShown
115+
116+ property int oldControlTimerInterval: 0;
117+
118+ function init() {
119+ services.sourceData = root.sourceData
120+ services.context = "video";
121+
122+ var controlHideTimer = findInvisibleChild(services, "controlHideTimer");
123+ verify(controlHideTimer !== null);
124+ oldControlTimerInterval = controlHideTimer.interval;
125+ }
126+
127+ function cleanup() {
128+ serviceCloseSpy.clear();
129+ services.context = "";
130+ services.fullscreen = false;
131+ services.maximumEmbeddedHeight = undefined;
132+ tryCompareFunction(function() { return services.header ? services.header.visible : false }, false);
133+ tryCompareFunction(function() { return services.footer ? services.footer.visible : false }, false);
134+
135+ var controlHideTimer = findInvisibleChild(services, "controlHideTimer");
136+ controlHideTimer.interval = oldControlTimerInterval;
137+ }
138+
139+ function test_videoWidget() {
140+ var videoPlayer = findChild(services, "videoPlayer");
141+ verify(videoPlayer !== null);
142+
143+ var videoControls = findChild(services, "videoControls");
144+ verify(videoControls !== null);
145+ }
146+
147+ function test_tapVideoPlayerToPlayAndPause() {
148+ var videoPlayer = findChild(services, "videoPlayer");
149+ verify(videoPlayer !== null)
150+ tap(videoPlayer, videoPlayer.width/2, videoPlayer.height/2);
151+
152+ var mediaPlayer = findInvisibleChild(services, "mediaPlayer");
153+ verify(mediaPlayer !== null);
154+ compare(mediaPlayer.playbackState, MediaPlayer.PlayingState, "Media player should be playing");
155+
156+ tap(videoPlayer, videoPlayer.width/2, videoPlayer.height/2);
157+ compare(mediaPlayer.playbackState, MediaPlayer.PausedState, "Media player should be playing");
158+ }
159+
160+ function test_fullscreenSwitch() {
161+ var mediaPlayer = findInvisibleChild(services, "mediaPlayer");
162+ verify(mediaPlayer !== null);
163+ mediaPlayer.play();
164+ wait(UbuntuAnimation.BriskDuration); // animation
165+
166+ var button = findChild(services, "viewActionButton");
167+ verify(button !== null);
168+ tap(button);
169+
170+ compare(services.fullscreen, true, "Should have switched to fullscreen mode.");
171+ }
172+
173+ function test_HeaderVisibleOnlyWhenFullscreen() {
174+ services.fullscreen = false;
175+ compare(services.header, null, "Header should be null when not fullscreen");
176+ services.fullscreen = true;
177+ tryCompareFunction(function() { return services.header !== null }, true);
178+ }
179+
180+ function test_ControlsShowAndHideWhenPlayed() {
181+ services.fullscreen = true;
182+ var controlHideTimer = findInvisibleChild(services, "controlHideTimer");
183+ controlHideTimer.interval = 100;
184+
185+ var mediaPlayer = findInvisibleChild(services, "mediaPlayer");
186+ mediaPlayer.play();
187+
188+ tryCompare(services.header, "visible", true);
189+ tryCompare(services.footer, "visible", true);
190+
191+ tryCompare(services.header, "visible", false);
192+ tryCompare(services.footer, "visible", false);
193+ }
194+
195+ function test_ControlsDontTimeOutWhenPaused() {
196+ services.fullscreen = true;
197+ var controlHideTimer = findInvisibleChild(services, "controlHideTimer");
198+ controlHideTimer.interval = 100;
199+
200+ var mediaPlayer = findInvisibleChild(services, "mediaPlayer");
201+ mediaPlayer.play();
202+ mediaPlayer.pause();
203+ wait(300);
204+
205+ compare(services.header.visible, true, "Header should still be visible");
206+ compare(services.footer.visible, true, "Footer should still be visible");
207+ }
208+
209+ function test_close() {
210+ services.fullscreen = true;
211+ var mediaPlayer = findInvisibleChild(services, "mediaPlayer");
212+ mediaPlayer.play();
213+ mediaPlayer.pause();
214+ wait(UbuntuAnimation.BriskDuration); // animation
215+
216+ var navigationButton = findChild(services, "navigationButton");
217+ verify(navigationButton !== null);
218+
219+ tap(navigationButton);
220+ compare(serviceCloseSpy.count, 1, "close was not called");
221+ }
222+
223+ function test_maximumVideoSize() {
224+ services.maximumEmbeddedHeight = root.height / 2
225+ services.sourceData = root.sourceData2
226+ verify(services.content !== null);
227+ tryCompare(services.content, "height", root.height/2);
228+ }
229+ }
230+}
tests/qmltests/Dash/Previews/tst_PreviewSharing.qml → tests/qmltests/Components/tst_SharingPicker.qmlrenamed+8−8
@@ -17,7 +17,7 @@
1717import QtQuick 2.4
1818import QtTest 1.0
1919import Ubuntu.Components 1.3
20−import "../../../../qml/Dash/Previews"
20+import "../../../qml/Components"
2121import Unity.Test 0.1 as UT
2222
2323Rectangle {
@@ -40,33 +40,33 @@ Rectangle {
4040 "content-type": "text"
4141 }
4242
43− PreviewSharing {
44− id: previewSharing
43+ SharingPicker {
44+ id: sharingPicker
4545 anchors { left: parent.left; bottom: parent.bottom; }
4646 shareData: root.shareData
4747 }
4848
4949 UT.UnityTestCase {
50− name: "PreviewSharingTest"
50+ name: "SharingPickerTest"
5151 when: windowShown
5252
53− property Item peerPicker: findChild(previewSharing.rootItem, "peerPicker")
53+ property Item peerPicker: findChild(sharingPicker.rootItem, "peerPicker")
5454
5555 function cleanup() {
5656 peerPicker.visible = false;
5757 }
5858
5959 function test_open_picker() {
60− mouseClick(previewSharing);
60+ sharingPicker.showPeerPicker();
6161 compare(peerPicker.visible, true);
6262 }
6363
6464 function test_createExportedItems() {
65− var exportedItems = previewSharing.createExportedItems(shareData["uri"]);
65+ var exportedItems = sharingPicker.createExportedItems(shareData["uri"]);
6666 for (var i = 0; i < exportedItems.length; i++) {
6767 compare(exportedItems[i].url, shareData["uri"][i]);
6868 }
69− exportedItems = previewSharing.createExportedItems(shareDataString["uri"]);
69+ exportedItems = sharingPicker.createExportedItems(shareDataString["uri"]);
7070 compare(exportedItems[0].url, shareDataString["uri"]);
7171 }
7272 }
tests/qmltests/Dash/Previews/tst_PreviewInlineVideo.qmlnew file+114
@@ -0,0 +1,114 @@
1+/*
2+ * Copyright 2016 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import QtTest 1.0
19+import "../../../../qml/Dash/Previews"
20+import Unity.Test 0.1 as UT
21+import Ubuntu.Components 1.3
22+import QtMultimedia 5.0
23+
24+Rectangle {
25+ width: units.gu(70)
26+ height: units.gu(80)
27+ color: "lightgrey"
28+
29+ property var widgetData0: {
30+ "source": "file:///test-video1",
31+ "screenshot": Qt.resolvedUrl("../artwork/avatar.png"),
32+ "share-data": {
33+ "uri": [
34+ "file:///this/is/an/url",
35+ "file:///this/is/an/url/2",
36+ "file:///this/is/an/url/3"
37+ ],
38+ "content-type": "text"
39+ }
40+ }
41+
42+ property var widgetData1: {
43+ "source": "file:///test-video2",
44+ "screenshot": "file:///test-video2-screenshot"
45+ }
46+
47+ MediaDataSource {
48+ source: "file:///test-video1"
49+ duration: 6000000
50+ metaData: {
51+ "title" : "TEST MPEG",
52+ "resolution" : { "width": 1920, "height": 1080 }
53+ }
54+ }
55+
56+ Item {
57+ anchors.fill: parent
58+
59+ Item {
60+ id: inner
61+ anchors {
62+ top: parent.top
63+ bottom: parent.bottom
64+ left: parent.left
65+ right: controls.left
66+ }
67+
68+ Item {
69+ anchors.fill: parent
70+ anchors.margins: units.gu(2)
71+
72+ PreviewInlineVideo {
73+ id: videoPlayback
74+ width: parent.width
75+ widgetData: widgetData0
76+
77+ rootItem: inner
78+ }
79+ }
80+
81+ }
82+
83+ Rectangle {
84+ id: controls
85+ color: "darkgrey"
86+ anchors {
87+ top: parent.top
88+ bottom: parent.bottom
89+ right: parent.right
90+ }
91+ width: units.gu(30)
92+
93+ Column {
94+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
95+
96+ UT.MouseTouchEmulationCheckbox {}
97+ }
98+ }
99+ }
100+
101+ UT.UnityTestCase {
102+ name: "PreviewInlineVideoTest"
103+ when: windowShown
104+
105+ function test_loadScreenshot() {
106+ var screenshot = findChild(videoPlayback, "screenshot");
107+ verify(screenshot !== null);
108+
109+ videoPlayback.widgetData = widgetData1;
110+ var screenshotSource = screenshot.source
111+ compare(screenshotSource.toString(), "file:///test-video2-screenshot");
112+ }
113+ }
114+}
tests/qmltests/Dash/Previews/tst_PreviewMediaToolbar.qml+3−3
@@ -51,7 +51,7 @@ Rectangle {
5151 name: "PreviewMediaToolbarTest"
5252 when: windowShown
5353
54− property Item sharingWidget: findChild(previewMediaToolbar, "sharingWidget")
54+ property Item sharingPicker: findChild(previewMediaToolbar, "sharingPicker")
5555
5656 function cleanup() {
5757 previewMediaToolbar.shareData = root.shareData;
@@ -60,10 +60,10 @@ Rectangle {
6060 function test_visible() {
6161 previewMediaToolbar.shareData = shareDataNoUri;
6262 compare(previewMediaToolbar.visible, false);
63− compare(sharingWidget.visible, false);
63+ compare(sharingPicker.visible, false);
6464 previewMediaToolbar.shareData = shareData;
6565 compare(previewMediaToolbar.visible, true);
66− compare(sharingWidget.visible, true);
66+ compare(sharingPicker.visible, true);
6767 }
6868 }
6969}
tests/qmltests/Dash/Previews/tst_PreviewWidgetFactory.qml+17−16
@@ -75,26 +75,27 @@ Rectangle {
7575
7676 function test_mapping_data() {
7777 return [
78− { tag: "Actions", type: "actions", source: "PreviewActions.qml", expanded: true },
79− { tag: "Audio", type: "audio", source: "PreviewAudioPlayback.qml", expanded: true },
80− { tag: "Comment", type: "comment", source: "PreviewComment.qml", expanded: true },
81− { tag: "Comment Input", type: "comment-input", source: "PreviewCommentInput.qml", expanded: true },
82− { tag: "Expandable", type: "expandable", source: "PreviewExpandable.qml", expanded: false },
83− { tag: "Gallery", type: "gallery", source: "PreviewImageGallery.qml", expanded: true },
84− { tag: "Header", type: "header", source: "PreviewHeader.qml", expanded: true },
85− { tag: "Image", type: "image", source: "PreviewZoomableImage.qml", expanded: true },
86− { tag: "Progress", type: "progress", source: "PreviewProgress.qml", expanded: true },
87− { tag: "Rating Input", type: "rating-input", source: "PreviewRatingInput.qml", expanded: true },
88− { tag: "Rating Display", type: "reviews", source: "PreviewRatingDisplay.qml", expanded: true },
89− { tag: "Table", type: "table", source: "PreviewTable.qml", expanded: true },
90− { tag: "Text", type: "text", source: "PreviewTextSummary.qml", expanded: true },
91− { tag: "Video", type: "video", source: "PreviewVideoPlayback.qml", expanded: true },
78+ { tag: "Actions", data: { type: "actions" }, source: "PreviewActions.qml", expanded: true },
79+ { tag: "Audio", data: { type: "audio" }, source: "PreviewAudioPlayback.qml", expanded: true },
80+ { tag: "Comment", data: { type: "comment" }, source: "PreviewComment.qml", expanded: true },
81+ { tag: "Comment Input", data: { type: "comment-input" }, source: "PreviewCommentInput.qml", expanded: true },
82+ { tag: "Expandable", data: { type: "expandable" }, source: "PreviewExpandable.qml", expanded: false },
83+ { tag: "Gallery", data: { type: "gallery" }, source: "PreviewImageGallery.qml", expanded: true },
84+ { tag: "Header", data: { type: "header" }, source: "PreviewHeader.qml", expanded: true },
85+ { tag: "Image", data: { type: "image" }, source: "PreviewZoomableImage.qml", expanded: true },
86+ { tag: "Progress", data: { type: "progress" }, source: "PreviewProgress.qml", expanded: true },
87+ { tag: "Rating Input", data: { type: "rating-input" }, source: "PreviewRatingInput.qml", expanded: true },
88+ { tag: "Rating Display", data: { type: "reviews" }, source: "PreviewRatingDisplay.qml", expanded: true },
89+ { tag: "Table", data: { type: "table" }, source: "PreviewTable.qml", expanded: true },
90+ { tag: "Text", data: { type: "text" }, source: "PreviewTextSummary.qml", expanded: true },
91+ { tag: "Video Inline", data: { type: "video", source: "httpDemo" }, source: "PreviewInlineVideo.qml", expanded: true },
92+ { tag: "Video", data: { type: "video", source: "https://demo" }, source: "PreviewVideoPlayback.qml", expanded: true },
9293 ];
9394 }
9495
9596 function test_mapping(data) {
96− factory.widgetData = { type: data.type };
97− factory.widgetType = data.type;
97+ factory.widgetData = data.data;
98+ factory.widgetType = data.data.type;
9899
99100 verify((String(factory.source)).indexOf(data.source) != -1);
100101 compare(factory.item.expanded, data.expanded);