Merge lp:~nick-dedekind/unity8/inline-dash-videos into lp:unity8
- inline-dash-videos
- Merge into trunk
Status: | Superseded | ||||
---|---|---|---|---|---|
Proposed branch: | lp:~nick-dedekind/unity8/inline-dash-videos | ||||
Merge into: | lp:unity8 | ||||
Diff against target: |
2432 lines (+2089/-59) 20 files modified
qml/Components/LazyImage.qml (+2/-0) qml/Components/MediaServices/MediaServices.qml (+391/-0) qml/Components/MediaServices/MediaServicesControls.qml (+189/-0) qml/Components/MediaServices/MediaServicesHeader.qml (+72/-0) qml/Components/MediaServices/VideoPlayer.qml (+152/-0) qml/Components/MediaServices/VideoPlayerControls.qml (+182/-0) qml/Dash/GenericScopeView.qml (+1/-0) qml/Dash/Previews/PreviewInlineVideo.qml (+89/-0) qml/Dash/Previews/PreviewWidgetFactory.qml (+8/-1) tests/mocks/QtMultimedia/CMakeLists.txt (+8/-2) tests/mocks/QtMultimedia/VideoSurface.qml (+111/-0) tests/mocks/QtMultimedia/mediaplayer.cpp (+216/-29) tests/mocks/QtMultimedia/mediaplayer.h (+131/-10) tests/mocks/QtMultimedia/plugin.cpp (+7/-3) tests/mocks/QtMultimedia/videooutput.cpp (+127/-0) tests/mocks/QtMultimedia/videooutput.h (+52/-0) tests/qmltests/CMakeLists.txt (+2/-0) tests/qmltests/Components/tst_MediaServices.qml (+228/-0) tests/qmltests/Dash/Previews/tst_PreviewInlineVideo.qml (+106/-0) tests/qmltests/Dash/Previews/tst_PreviewWidgetFactory.qml (+15/-14) |
||||
To merge this branch: | bzr merge lp:~nick-dedekind/unity8/inline-dash-videos | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michał Sawicz | Needs Fixing | ||
Albert Astals Cid (community) | Needs Fixing | ||
PS Jenkins bot (community) | continuous-integration | Needs Fixing | |
Jim Hodapp (community) | Needs Fixing | ||
Review via email: mp+260251@code.launchpad.net |
This proposal has been superseded by a proposal from 2015-12-15.
Commit message
Added live video playback in dash previews
Description of the change
Added live video playback in dash previews.
https:/
* Are there any related MPs required for this MP to build/function as expected? Please list.
N/A
* Did you perform an exploratory manual test run of your code change and any related functionality?
Yes
* Did you make sure that your branch does not contain spurious tags?
Yes
* If you changed the packaging (debian), did you subscribe the ubuntu-unity team to this MP?
N/A
* If you changed the UI, has there been a design review?
Not yet.
* Did you have a look at the warnings when running tests? Can they be reduced?
N/A
Nick Dedekind (nick-dedekind) wrote : | # |
- 1781. By Nick Dedekind
-
removed QtMultimedia req
- 1782. By Nick Dedekind
-
reverted req removal
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1782
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
Albert Astals Cid (aacid) wrote : | # |
Missing copyright headers on the new files
- 1783. By Nick Dedekind
-
re-remove qtmultimedia req!
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1783
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 1784. By Nick Dedekind
-
update components version
- 1785. By Nick Dedekind
-
added copywrite. removed unused code. error handling
Nick Dedekind (nick-dedekind) wrote : | # |
> Missing copyright headers on the new files
added
- 1786. By Nick Dedekind
-
more headers
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1784
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1786
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1786
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Jim Hodapp (jhodapp) wrote : | # |
Need to clarify with the design team about what the user experience should be when a video is larger than the size of the screen. For example, record a video using the camera-app and then preview the video in this new scope code. You'll notice it doesn't fit on the screen and is a pretty poor UX. I don't think the default behavior is desirable but should the VideoOutput size be scaled down to fit or what?
Michael Zanetti (mzanetti) wrote : | # |
> Need to clarify with the design team about what the user experience should be
> when a video is larger than the size of the screen. For example, record a
> video using the camera-app and then preview the video in this new scope code.
> You'll notice it doesn't fit on the screen and is a pretty poor UX. I don't
> think the default behavior is desirable but should the VideoOutput size be
> scaled down to fit or what?
PreserveAspectFit, can't see any other reasonable option.
Jim Hodapp (jhodapp) wrote : | # |
Why wouldn't stretch while shrinking the size of the VideoOutput to shrink it be an option? As long as we shrink it preserving the aspect ratio, it should still look just fine since you're not stretching it larger.
Nick Dedekind (nick-dedekind) wrote : | # |
> Why wouldn't stretch while shrinking the size of the VideoOutput to shrink it
> be an option? As long as we shrink it preserving the aspect ratio, it should
> still look just fine since you're not stretching it larger.
Not quite sure what you mean here. What are we stretching?
We would scale the video output down to the maximum available space while keeping aspect.
Jim Hodapp (jhodapp) wrote : | # |
> > Why wouldn't stretch while shrinking the size of the VideoOutput to shrink
> it
> > be an option? As long as we shrink it preserving the aspect ratio, it should
> > still look just fine since you're not stretching it larger.
>
> Not quite sure what you mean here. What are we stretching?
> We would scale the video output down to the maximum available space while
> keeping aspect.
That's precisely what I mean. Apparently it was a bad word day for me. ;)
- 1787. By Nick Dedekind
-
merged trunk
- 1788. By Nick Dedekind
-
better handling of sizing
- 1789. By Nick Dedekind
-
better lazy image scaling
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1789
http://
Executed test runs:
FAILURE: http://
SUCCESS: http://
SUCCESS: http://
FAILURE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Albert Astals Cid (aacid) wrote : | # |
You have bad tags
Albert Astals Cid (aacid) wrote : | # |
Text conflict in tests/qmltests/
1 conflicts encountered.
- 1790. By Nick Dedekind
-
merged with trunk
Nick Dedekind (nick-dedekind) wrote : | # |
> You have bad tags
fixed and merged.
Albert Astals Cid (aacid) : | # |
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1790
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 1791. By Nick Dedekind
-
reverted to sdk 1.2
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1791
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Albert Astals Cid (aacid) wrote : | # |
Should we remove PreviewVideoPla
Albert Astals Cid (aacid) wrote : | # |
There's a tst_MediaServic
make testMediaServices.
Albert Astals Cid (aacid) wrote : | # |
When "fullscreen" one can see the "preview page" below (see Video in http://
Albert Astals Cid (aacid) wrote : | # |
Text conflict in tests/mocks/
Text conflict in tests/mocks/
2 conflicts encountered.
Albert Astals Cid (aacid) wrote : | # |
Change the TRANSLATORS to Translators it's what we settled on because previously we were using both and gettext only supports one.
Also run make pot_file to have an updated .pot
Nick Dedekind (nick-dedekind) wrote : | # |
> When "fullscreen" one can see the "preview page" below (see Video in
> http://
> to me.
I followed the designs and colors specified in https:/
So I guess the answer is yes, until we're told otherwise anyway.
- 1792. By Nick Dedekind
-
merged with trunk
- 1793. By Nick Dedekind
-
moved inline video to old files
- 1794. By Nick Dedekind
-
updated translations
Nick Dedekind (nick-dedekind) wrote : | # |
Fixed comments.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1794
http://
Executed test runs:
FAILURE: http://
SUCCESS: http://
SUCCESS: http://
FAILURE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Albert Astals Cid (aacid) wrote : | # |
The preview looks weird before the thumbnail arrives
http://
Maybe we need some max in the height that also takes into account the play button?
- 1795. By Nick Dedekind
-
reverted file changes
- 1796. By Nick Dedekind
-
use both inline and old video widget
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1796
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Nick Dedekind (nick-dedekind) wrote : | # |
> The preview looks weird before the thumbnail arrives
>
> http://
>
> Maybe we need some max in the height that also takes into account the play
> button?
Should be fixed now. Initial screenshot height is playButton height + gu(2) margin.
Albert Astals Cid (aacid) wrote : | # |
going back and forth from to a preview of a video that i recorded with the phone ends up in crashes in media-hub and that freezes the dash.
Can we make the media-hub crashing to freeze the dash? Are we waiting for something in a sync manner that should be async?
Jim Hodapp (jhodapp) wrote : | # |
Albert: Can you get a back trace of all of the threads plus a threads info
listing and the media-hub.log for the crash?
On Tue, Jul 14, 2015 at 7:23 AM, Albert Astals Cid <
<email address hidden>> wrote:
> Review: Needs Fixing
>
> going back and forth from to a preview of a video that i recorded with the
> phone ends up in crashes in media-hub and that freezes the dash.
>
> Can we make the media-hub crashing to freeze the dash? Are we waiting for
> something in a sync manner that should be async?
> --
>
> https:/
> You are reviewing the proposed merge of
> lp:~nick-dedekind/unity8/inline-dash-videos into lp:unity8.
>
Albert Astals Cid (aacid) wrote : | # |
> Albert: Can you get a back trace of all of the threads plus a threads info
> listing and the media-hub.log for the crash?
Jim: here it comes
bt of threads: http://
media-hub.log: http://
Jim Hodapp (jhodapp) wrote : | # |
That looks good, thanks Albert
On Tue, Jul 14, 2015 at 11:12 AM, Albert Astals Cid <
<email address hidden>> wrote:
> > Albert: Can you get a back trace of all of the threads plus a threads
> info
> > listing and the media-hub.log for the crash?
>
> Jim: here it comes
>
> bt of threads: http://
> media-hub.log: http://
> --
>
> https:/
> You are reviewing the proposed merge of
> lp:~nick-dedekind/unity8/inline-dash-videos into lp:unity8.
>
Albert Astals Cid (aacid) wrote : | # |
Text conflict in tests/qmltests/
1 conflicts encountered.
Albert Astals Cid (aacid) wrote : | # |
You can probably remove
item.
Once we merge
https:/
Michael Zanetti (mzanetti) wrote : | # |
mardy's branch is merged.
Albert Astals Cid (aacid) wrote : | # |
Please propose against overlay
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1796
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
Albert Astals Cid (aacid) wrote : | # |
Ignore the proposing to overlay, we don't use it anymore, you still have conflicts though
Text conflict in qml/Dash/
Text conflict in tests/mocks/
Text conflict in tests/qmltests/
3 conflicts encountered.
Michał Sawicz (saviq) wrote : | # |
Text conflict in qml/Dash/
Text conflict in tests/mocks/
Text conflict in tests/qmltests/
3 conflicts encountered.
Unmerged revisions
Preview Diff
1 | === modified file 'qml/Components/LazyImage.qml' |
2 | --- qml/Components/LazyImage.qml 2014-09-29 20:55:54 +0000 |
3 | +++ qml/Components/LazyImage.qml 2015-07-10 14:23:36 +0000 |
4 | @@ -29,6 +29,7 @@ |
5 | |
6 | property real initialWidth: scaleTo == "width" || scaleTo == "fit" ? width : units.gu(10) |
7 | property real initialHeight: scaleTo == "height" || scaleTo == "fit" ? height : units.gu(10) |
8 | + property real lastScaledDimension: scaleTo == "height" || scaleTo == "fit" ? width : height |
9 | |
10 | property alias sourceSize: image.sourceSize |
11 | property alias fillMode: image.fillMode |
12 | @@ -157,6 +158,7 @@ |
13 | easing.type: Easing.Linear; duration: UbuntuAnimation.SnapDuration |
14 | } |
15 | } |
16 | + ScriptAction { script: { lastScaledDimension = scaleTo == "height" || scaleTo == "fit" ? root.width : root.height } } |
17 | } |
18 | }, |
19 | |
20 | |
21 | === added directory 'qml/Components/MediaServices' |
22 | === added file 'qml/Components/MediaServices/MediaServices.qml' |
23 | --- qml/Components/MediaServices/MediaServices.qml 1970-01-01 00:00:00 +0000 |
24 | +++ qml/Components/MediaServices/MediaServices.qml 2015-07-10 14:23:36 +0000 |
25 | @@ -0,0 +1,391 @@ |
26 | +/* |
27 | + * Copyright (C) 2015 Canonical, Ltd. |
28 | + * |
29 | + * This program is free software; you can redistribute it and/or modify |
30 | + * it under the terms of the GNU General Public License as published by |
31 | + * the Free Software Foundation; version 3. |
32 | + * |
33 | + * This program is distributed in the hope that it will be useful, |
34 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
35 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
36 | + * GNU General Public License for more details. |
37 | + * |
38 | + * You should have received a copy of the GNU General Public License |
39 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
40 | + */ |
41 | + |
42 | +import QtQuick 2.0 |
43 | +import QtMultimedia 5.0 |
44 | +import Ubuntu.Components 1.2 |
45 | + |
46 | +FocusScope { |
47 | + id: root |
48 | + property var sourceData |
49 | + property string context: "" |
50 | + property list<Action> actions |
51 | + property Item rootItem: QuickUtils.rootItem(root) |
52 | + property var maximumEmbeddedHeight |
53 | + |
54 | + property alias header: headerContent.item |
55 | + property alias content: contentLoader.item |
56 | + property alias footer: footerContent.item |
57 | + property alias fullscreen: priv.fullscreen |
58 | + |
59 | + signal close(); |
60 | + |
61 | + onFullscreenChanged: { |
62 | + if (!fullscreen) rotationAction.checked = false; |
63 | + } |
64 | + |
65 | + OrientationHelper { |
66 | + id: orientationHelper |
67 | + automaticOrientation: fullscreen |
68 | + |
69 | + StateGroup { |
70 | + id: orientationState |
71 | + |
72 | + states: [ |
73 | + State { |
74 | + name: "portrait" |
75 | + when: !rotationAction.checked |
76 | + }, |
77 | + State { |
78 | + name: "landscape" |
79 | + when: rotationAction.checked |
80 | + } |
81 | + ] |
82 | + |
83 | + transitions: [ |
84 | + Transition { |
85 | + to: "landscape" |
86 | + SequentialAnimation { |
87 | + PropertyAction { target: orientationHelper; property: "automaticOrientation"; value: false } |
88 | + PropertyAction { target: orientationHelper; property: "orientationAngle"; value: 90 } |
89 | + PropertyAction { target: orientationHelper; property: "automaticOrientation"; value: false } |
90 | + } |
91 | + }, |
92 | + Transition { |
93 | + to: "portrait" |
94 | + SequentialAnimation { |
95 | + PropertyAction { target: orientationHelper; property: "automaticOrientation"; value: false } |
96 | + PropertyAction { target: orientationHelper; property: "orientationAngle"; value: 0 } |
97 | + PropertyAction { target: orientationHelper; property: "automaticOrientation"; value: fullscreen } |
98 | + } |
99 | + } |
100 | + ] |
101 | + } |
102 | + |
103 | + Loader { |
104 | + id: contentLoader |
105 | + |
106 | + sourceComponent: { |
107 | + switch (context) { |
108 | + case "video": |
109 | + return videoComponent; |
110 | + } |
111 | + return undefined; |
112 | + } |
113 | + |
114 | + Rectangle { |
115 | + anchors.fill: parent |
116 | + color: "#1B1B1B" |
117 | + opacity: 0.85 |
118 | + } |
119 | + |
120 | + Component { |
121 | + id: videoComponent |
122 | + VideoPlayer { |
123 | + id: player |
124 | + objectName: "videoPlayer" |
125 | + |
126 | + width: orientationHelper.width |
127 | + height: orientationHelper.height |
128 | + maximumEmbeddedHeight: root.maximumEmbeddedHeight |
129 | + fixedHeight: fullscreen |
130 | + orientation: orientationState.state == "landscape" ? Qt.LandscapeOrientation : Qt.PortraitOrientation |
131 | + |
132 | + screenshot: { |
133 | + var screenshot = root.sourceData["screenshot"]; |
134 | + if (screenshot) return screenshot; |
135 | + |
136 | + var source = root.sourceData["source"]; |
137 | + if (source) { |
138 | + if (source.toString().indexOf("file://") === 0) { |
139 | + return "image://thumbnailer/" + source.toString().substr(7); |
140 | + } |
141 | + } |
142 | + return ""; |
143 | + } |
144 | + |
145 | + mediaPlayer: footer ? footer.mediaPlayer : null |
146 | + |
147 | + onClicked: { |
148 | + if (mediaPlayer.availability !== MediaPlayer.Available) return; |
149 | + |
150 | + if (mediaPlayer.playbackState === MediaPlayer.StoppedState) { |
151 | + mediaPlayer.play(); |
152 | + } else if (controlHideTimer.running) { |
153 | + if (mediaPlayer.playbackState === MediaPlayer.PlayingState) { |
154 | + mediaPlayer.pause(); |
155 | + } else { |
156 | + mediaPlayer.play(); |
157 | + } |
158 | + } else { |
159 | + controlHideTimer.restart(); |
160 | + } |
161 | + } |
162 | + onPositionChanged: controlHideTimer.restart(); |
163 | + } |
164 | + } |
165 | + } |
166 | + |
167 | + Loader { |
168 | + id: headerContent |
169 | + anchors { |
170 | + top: contentLoader.top |
171 | + left: parent.left |
172 | + right: parent.right |
173 | + topMargin: -units.gu(6) |
174 | + } |
175 | + height: units.gu(6) |
176 | + visible: false |
177 | + |
178 | + // eater |
179 | + MouseArea { |
180 | + anchors.fill: parent |
181 | + } |
182 | + |
183 | + Rectangle { |
184 | + anchors.fill: parent |
185 | + color: "#1B1B1B" |
186 | + opacity: 0.85 |
187 | + visible: headerContent.status === Loader.Ready |
188 | + } |
189 | + |
190 | + sourceComponent: root.fullscreen ? headerComponent : undefined |
191 | + |
192 | + Component { |
193 | + id: headerComponent |
194 | + |
195 | + MediaServicesHeader { |
196 | + onGoPrevious: { |
197 | + rotationAction.checked = false; |
198 | + root.close(); |
199 | + } |
200 | + |
201 | + component: { |
202 | + switch (context) { |
203 | + case "video": |
204 | + break; |
205 | + } |
206 | + return undefined; |
207 | + } |
208 | + } |
209 | + } |
210 | + |
211 | + // If we interact with the bar, reset the hide timer. |
212 | + MouseArea { |
213 | + anchors.fill: parent |
214 | + onPressed: { |
215 | + mouse.accepted = false |
216 | + if (controlHideTimer.running) controlHideTimer.restart() |
217 | + } |
218 | + } |
219 | + } |
220 | + |
221 | + Loader { |
222 | + id: footerContent |
223 | + anchors { |
224 | + left: parent.left |
225 | + right: parent.right |
226 | + bottom: contentLoader.bottom |
227 | + bottomMargin: -units.gu(7) |
228 | + } |
229 | + height: units.gu(7) |
230 | + visible: false |
231 | + |
232 | + sourceComponent: { |
233 | + switch (context) { |
234 | + case "video": |
235 | + return videoControlsComponent; |
236 | + } |
237 | + return undefined; |
238 | + } |
239 | + // eater |
240 | + MouseArea { |
241 | + anchors.fill: parent |
242 | + } |
243 | + |
244 | + Rectangle { |
245 | + anchors.fill: parent |
246 | + color: "#1B1B1B" |
247 | + opacity: 0.85 |
248 | + visible: footerContent.status === Loader.Ready |
249 | + } |
250 | + |
251 | + Component { |
252 | + id: videoControlsComponent |
253 | + VideoPlayerControls { |
254 | + id: controls |
255 | + objectName: "videoControls" |
256 | + |
257 | + viewAction: rotationAction.enabled ? rotationAction : fullscreenAction |
258 | + userActions: root.actions |
259 | + |
260 | + mediaPlayer.source: { |
261 | + if (!root.sourceData) return ""; |
262 | + |
263 | + var x = root.sourceData["source"]; |
264 | + if (x.toString().indexOf("video://") === 0) { |
265 | + return x.toString().substr(6); |
266 | + } |
267 | + return x; |
268 | + } |
269 | + mediaPlayer.onPlaybackStateChanged: { |
270 | + controlHideTimer.restart(); |
271 | + } |
272 | + |
273 | + Binding { |
274 | + target: priv |
275 | + property: "forceControlsShown" |
276 | + |
277 | + value: (fullscreen && mediaPlayer.playbackState === MediaPlayer.StoppedState) || |
278 | + mediaPlayer.playbackState === MediaPlayer.PausedState || |
279 | + interacting |
280 | + } |
281 | + |
282 | + onInteractingChanged: { |
283 | + controlHideTimer.restart(); |
284 | + } |
285 | + |
286 | + Binding { |
287 | + target: header |
288 | + property: "title" |
289 | + value: controls.mediaPlayer.metaData.title !== undefined ? |
290 | + controls.mediaPlayer.metaData.title : |
291 | + controls.mediaPlayer.source.toString().replace(/^.*[\\\/]/, '') |
292 | + when: header != null |
293 | + } |
294 | + } |
295 | + } |
296 | + |
297 | + // If we interact with the bar, reset the hide timer. |
298 | + MouseArea { |
299 | + z: 1 |
300 | + anchors.fill: parent |
301 | + onPressed: { |
302 | + mouse.accepted = false |
303 | + if (controlHideTimer.running) controlHideTimer.restart() |
304 | + } |
305 | + } |
306 | + } |
307 | + } |
308 | + |
309 | + StateGroup { |
310 | + states: [ |
311 | + State { |
312 | + name: "controlsShown" |
313 | + when: priv.forceControlsShown || priv.controlTimerActive |
314 | + PropertyChanges { |
315 | + target: footerContent |
316 | + anchors.bottomMargin: 0 |
317 | + visible: true |
318 | + } |
319 | + PropertyChanges { |
320 | + target: headerContent |
321 | + anchors.topMargin: 0 |
322 | + visible: true |
323 | + } |
324 | + } |
325 | + ] |
326 | + |
327 | + transitions: [ |
328 | + Transition { |
329 | + to: "controlsShown" |
330 | + SequentialAnimation { |
331 | + PropertyAction { target: root; property: "clip"; value: true } |
332 | + PropertyAction { property: "visible" } |
333 | + NumberAnimation { |
334 | + properties: "anchors.bottomMargin,anchors.topMargin" |
335 | + duration: UbuntuAnimation.FastDuration |
336 | + } |
337 | + PropertyAction { target: root; property: "clip"; value: false } |
338 | + } |
339 | + }, |
340 | + Transition { |
341 | + from: "controlsShown" |
342 | + SequentialAnimation { |
343 | + PropertyAction { target: root; property: "clip"; value: true } |
344 | + NumberAnimation { |
345 | + properties: "anchors.bottomMargin,anchors.topMargin" |
346 | + duration: UbuntuAnimation.FastDuration |
347 | + } |
348 | + PropertyAction { property: "visible" } |
349 | + PropertyAction { target: root; property: "clip"; value: false } |
350 | + } |
351 | + } |
352 | + ] |
353 | + } |
354 | + |
355 | + StateGroup { |
356 | + states: [ |
357 | + State { |
358 | + name: "minimized" |
359 | + when: !priv.fullscreen |
360 | + PropertyChanges { target: root; implicitHeight: content ? content.implicitHeight : 0; } |
361 | + }, |
362 | + State { |
363 | + name: "fullscreen" |
364 | + when: priv.fullscreen |
365 | + ParentChange { target: root; parent: rootItem; x: 0; y: 0; width: parent.width; } |
366 | + PropertyChanges { target: root; implicitHeight: root.parent ? root.parent.height : 0; } |
367 | + PropertyChanges { target: fullscreenAction; iconName: "view-restore" } |
368 | + } |
369 | + ] |
370 | + |
371 | + transitions: Transition { |
372 | + ParentAnimation { |
373 | + UbuntuNumberAnimation { properties: "x,y,width,implicitHeight"; duration: UbuntuAnimation.FastDuration } |
374 | + } |
375 | + } |
376 | + } |
377 | + |
378 | + QtObject { |
379 | + id: priv |
380 | + |
381 | + property bool fullscreen: false |
382 | + property bool controlTimerActive: false |
383 | + property bool forceControlsShown: false |
384 | + } |
385 | + |
386 | + Timer { |
387 | + id: controlHideTimer |
388 | + objectName: "controlHideTimer" |
389 | + interval: 4000 |
390 | + running: false |
391 | + |
392 | + onRunningChanged: { |
393 | + if (running) { |
394 | + priv.controlTimerActive = true; |
395 | + } |
396 | + } |
397 | + onTriggered: priv.controlTimerActive = false; |
398 | + } |
399 | + |
400 | + Action { |
401 | + id: fullscreenAction |
402 | + enabled: rotationAction.enabled === false |
403 | + iconName: "view-fullscreen" |
404 | + |
405 | + onTriggered: priv.fullscreen = !priv.fullscreen |
406 | + } |
407 | + |
408 | + Action { |
409 | + id: rotationAction |
410 | + enabled: root.fullscreen === true |
411 | + iconName: "orientation-lock" |
412 | + |
413 | + property bool checked: false |
414 | + onTriggered: checked = !checked |
415 | + } |
416 | +} |
417 | |
418 | === added file 'qml/Components/MediaServices/MediaServicesControls.qml' |
419 | --- qml/Components/MediaServices/MediaServicesControls.qml 1970-01-01 00:00:00 +0000 |
420 | +++ qml/Components/MediaServices/MediaServicesControls.qml 2015-07-10 14:23:36 +0000 |
421 | @@ -0,0 +1,189 @@ |
422 | +/* |
423 | + * Copyright (C) 2015 Canonical, Ltd. |
424 | + * |
425 | + * This program is free software; you can redistribute it and/or modify |
426 | + * it under the terms of the GNU General Public License as published by |
427 | + * the Free Software Foundation; version 3. |
428 | + * |
429 | + * This program is distributed in the hope that it will be useful, |
430 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
431 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
432 | + * GNU General Public License for more details. |
433 | + * |
434 | + * You should have received a copy of the GNU General Public License |
435 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
436 | + */ |
437 | + |
438 | +import QtQuick 2.0 |
439 | +import QtQuick.Layouts 1.1 |
440 | +import QtMultimedia 5.0 |
441 | +import Ubuntu.Components 1.2 |
442 | +import Ubuntu.Components.ListItems 1.0 as ListItem |
443 | + |
444 | +Item { |
445 | + id: root |
446 | + |
447 | + property alias component: loader.sourceComponent |
448 | + |
449 | + signal actionClicked(string action) |
450 | + signal viewModeClicked |
451 | + |
452 | + property list<Action> userActions |
453 | + property Action viewAction |
454 | + |
455 | + RowLayout { |
456 | + anchors { |
457 | + left: parent.left |
458 | + right: parent.right |
459 | + } |
460 | + anchors.verticalCenter: parent.verticalCenter |
461 | + anchors.margins: units.gu(2) |
462 | + spacing: units.gu(2) |
463 | + |
464 | + AbstractButton { |
465 | + id: actionButton |
466 | + Layout.preferredWidth: units.gu(3) |
467 | + Layout.preferredHeight: units.gu(3) |
468 | + Layout.alignment: Qt.AlignVCenter |
469 | + enabled: action && action.enabled |
470 | + |
471 | + Action { |
472 | + id: popupAction |
473 | + iconName: "navigation-menu" |
474 | + onTriggered: userActionsPopup.createObject(root, { "anchors.bottom": root.top }) |
475 | + } |
476 | + |
477 | + action: { |
478 | + switch (userActions.length) { |
479 | + case 0: |
480 | + return null; |
481 | + case 1: |
482 | + return userActions[0]; |
483 | + default: |
484 | + return popupAction; |
485 | + } |
486 | + } |
487 | + |
488 | + Icon { |
489 | + anchors.fill: parent |
490 | + visible: actionButton.action && actionButton.action.iconSource !== "" || false |
491 | + source: actionButton.action ? actionButton.action.iconSource : "" |
492 | + color: "#F3F3E7" |
493 | + opacity: actionButton.action && actionButton.action.enabled ? 1.0 : 0.5 |
494 | + } |
495 | + } |
496 | + |
497 | + Loader { |
498 | + id: loader |
499 | + Layout.fillWidth: true |
500 | + Layout.preferredHeight: units.gu(3) |
501 | + } |
502 | + |
503 | + AbstractButton { |
504 | + objectName: "viewActionButton" |
505 | + Layout.preferredWidth: units.gu(3) |
506 | + Layout.preferredHeight: units.gu(3) |
507 | + Layout.alignment: Qt.AlignVCenter |
508 | + enabled: viewAction.enabled |
509 | + action: viewAction |
510 | + |
511 | + Icon { |
512 | + anchors.fill: parent |
513 | + visible: viewAction.iconSource !== "" |
514 | + source: viewAction.iconSource |
515 | + color: "#F3F3E7" |
516 | + opacity: viewAction.enabled ? 1.0 : 0.5 |
517 | + } |
518 | + } |
519 | + } |
520 | + |
521 | + Component { |
522 | + id: userActionsPopup |
523 | + |
524 | + Rectangle { |
525 | + id: popup |
526 | + color: "#1B1B1B" |
527 | + width: userActionsColumn.width |
528 | + height: userActionsColumn.height |
529 | + |
530 | + InverseMouseArea { |
531 | + id: eventGrabber |
532 | + acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton |
533 | + anchors.fill: popup |
534 | + propagateComposedEvents: false |
535 | + onWheel: wheel.accepted = true |
536 | + |
537 | + onPressed: popup.destroy() |
538 | + } |
539 | + |
540 | + Column { |
541 | + id: userActionsColumn |
542 | + spacing: units.gu(1) |
543 | + width: units.gu(31) |
544 | + |
545 | + Repeater { |
546 | + id: actionRepeater |
547 | + model: userActions |
548 | + AbstractButton { |
549 | + action: modelData |
550 | + |
551 | + onClicked: popup.destroy() |
552 | + |
553 | + implicitHeight: units.gu(4) + bottomDividerLine.height |
554 | + width: parent ? parent.width : units.gu(31) |
555 | + |
556 | + Rectangle { |
557 | + visible: parent.pressed |
558 | + anchors { |
559 | + left: parent.left |
560 | + right: parent.right |
561 | + top: parent.top |
562 | + } |
563 | + height: parent.height - bottomDividerLine.height |
564 | + opacity: 0.5 |
565 | + } |
566 | + |
567 | + Icon { |
568 | + id: actionIcon |
569 | + visible: "" !== action.iconSource |
570 | + source: action.iconSource |
571 | + color: "#F3F3E7" |
572 | + anchors { |
573 | + verticalCenter: parent.verticalCenter |
574 | + verticalCenterOffset: units.dp(-1) |
575 | + left: parent.left |
576 | + leftMargin: units.gu(2) |
577 | + } |
578 | + width: units.gu(2) |
579 | + height: units.gu(2) |
580 | + opacity: action.enabled ? 1.0 : 0.5 |
581 | + } |
582 | + |
583 | + Label { |
584 | + anchors { |
585 | + verticalCenter: parent.verticalCenter |
586 | + verticalCenterOffset: units.dp(-1) |
587 | + left: actionIcon.visible ? actionIcon.right : parent.left |
588 | + leftMargin: units.gu(2) |
589 | + right: parent.right |
590 | + } |
591 | + // In the tabs overflow panel there are no icons, and the font-size |
592 | + // is medium as opposed to the small font-size in the actions overflow panel. |
593 | + fontSize: actionIcon.visible ? "small" : "medium" |
594 | + elide: Text.ElideRight |
595 | + text: action.text |
596 | + color: "#F3F3E7" |
597 | + opacity: action.enabled ? 1.0 : 0.5 |
598 | + } |
599 | + |
600 | + ListItem.ThinDivider { |
601 | + id: bottomDividerLine |
602 | + anchors.bottom: parent.bottom |
603 | + visible: index !== actionRepeater.count - 1 |
604 | + } |
605 | + } |
606 | + } |
607 | + } |
608 | + } |
609 | + } |
610 | +} |
611 | |
612 | === added file 'qml/Components/MediaServices/MediaServicesHeader.qml' |
613 | --- qml/Components/MediaServices/MediaServicesHeader.qml 1970-01-01 00:00:00 +0000 |
614 | +++ qml/Components/MediaServices/MediaServicesHeader.qml 2015-07-10 14:23:36 +0000 |
615 | @@ -0,0 +1,72 @@ |
616 | +/* |
617 | + * Copyright (C) 2015 Canonical, Ltd. |
618 | + * |
619 | + * This program is free software; you can redistribute it and/or modify |
620 | + * it under the terms of the GNU General Public License as published by |
621 | + * the Free Software Foundation; version 3. |
622 | + * |
623 | + * This program is distributed in the hope that it will be useful, |
624 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
625 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
626 | + * GNU General Public License for more details. |
627 | + * |
628 | + * You should have received a copy of the GNU General Public License |
629 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
630 | + */ |
631 | + |
632 | +import QtQuick 2.3 |
633 | +import QtQuick.Layouts 1.1 |
634 | +import Ubuntu.Components 1.2 |
635 | + |
636 | +Item { |
637 | + id: root |
638 | + |
639 | + property alias title: _title.text |
640 | + property alias component: loader.sourceComponent |
641 | + |
642 | + signal goPrevious |
643 | + |
644 | + RowLayout { |
645 | + id: row |
646 | + anchors { |
647 | + left: parent.left |
648 | + right: parent.right |
649 | + } |
650 | + anchors.verticalCenter: parent.verticalCenter |
651 | + anchors.margins: units.gu(2) |
652 | + spacing: units.gu(2) |
653 | + |
654 | + // eater |
655 | + AbstractButton { |
656 | + id: navigationButton |
657 | + objectName: "navigationButton" |
658 | + Layout.preferredWidth: units.gu(3) |
659 | + Layout.preferredHeight: units.gu(3) |
660 | + Layout.alignment: Qt.AlignVCenter |
661 | + |
662 | + Icon { |
663 | + anchors.fill: parent |
664 | + name: "go-previous" |
665 | + color: "#F3F3E7" |
666 | + } |
667 | + |
668 | + onTriggered: { |
669 | + root.goPrevious(); |
670 | + } |
671 | + } |
672 | + |
673 | + Label { |
674 | + id: _title |
675 | + Layout.fillWidth: true |
676 | + Layout.alignment: Qt.AlignVCenter |
677 | + elide: Text.ElideRight |
678 | + color: "#F3F3E7" |
679 | + } |
680 | + |
681 | + Loader { |
682 | + id: loader |
683 | + Layout.alignment: Qt.AlignVCenter |
684 | + Layout.preferredHeight: units.gu(3) |
685 | + } |
686 | + } |
687 | +} |
688 | |
689 | === added file 'qml/Components/MediaServices/VideoPlayer.qml' |
690 | --- qml/Components/MediaServices/VideoPlayer.qml 1970-01-01 00:00:00 +0000 |
691 | +++ qml/Components/MediaServices/VideoPlayer.qml 2015-07-10 14:23:36 +0000 |
692 | @@ -0,0 +1,152 @@ |
693 | +/* |
694 | + * Copyright (C) 2015 Canonical, Ltd. |
695 | + * |
696 | + * This program is free software; you can redistribute it and/or modify |
697 | + * it under the terms of the GNU General Public License as published by |
698 | + * the Free Software Foundation; version 3. |
699 | + * |
700 | + * This program is distributed in the hope that it will be useful, |
701 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
702 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
703 | + * GNU General Public License for more details. |
704 | + * |
705 | + * You should have received a copy of the GNU General Public License |
706 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
707 | + */ |
708 | + |
709 | +import QtQuick 2.0 |
710 | +import QtMultimedia 5.0 |
711 | +import Ubuntu.Components 0.1 |
712 | +import Ubuntu.Thumbnailer 0.1 |
713 | +import "../../Components" |
714 | + |
715 | +Item { |
716 | + id: root |
717 | + |
718 | + property alias screenshot: image.source |
719 | + property alias mediaPlayer: videoOutput.source |
720 | + property int orientation: Qt.PortraitOrientation |
721 | + property bool fixedHeight: false |
722 | + property var maximumEmbeddedHeight |
723 | + |
724 | + implicitHeight: { |
725 | + if (parent && orientation == Qt.LandscapeOrientation) { |
726 | + return parent.height; |
727 | + } |
728 | + return content.height; |
729 | + } |
730 | + |
731 | + signal clicked |
732 | + signal positionChanged |
733 | + |
734 | + Item { |
735 | + id: content |
736 | + anchors { |
737 | + left: parent.left |
738 | + right: parent.right |
739 | + verticalCenter: parent.verticalCenter |
740 | + } |
741 | + height: { |
742 | + if (root.orientation == Qt.LandscapeOrientation || fixedHeight) { |
743 | + return root.height; |
744 | + } |
745 | + var proposedHeight = videoOutput.height; |
746 | + if (maximumEmbeddedHeight !== undefined && maximumEmbeddedHeight < proposedHeight) { |
747 | + return maximumEmbeddedHeight; |
748 | + } |
749 | + return proposedHeight; |
750 | + } |
751 | + clip: image.height > videoOutput.height |
752 | + |
753 | + LazyImage { |
754 | + id: image |
755 | + objectName: "screenshot" |
756 | + anchors { |
757 | + left: parent.left |
758 | + right: parent.right |
759 | + } |
760 | + anchors.verticalCenter: parent.verticalCenter |
761 | + scaleTo: "width" |
762 | + lastScaledDimension: playButton.height + units.gu(2) |
763 | + initialHeight: lastScaledDimension |
764 | + |
765 | + visible: !mediaPlayer || mediaPlayer.playbackState === MediaPlayer.StoppedState |
766 | + } |
767 | + |
768 | + VideoOutput { |
769 | + id: videoOutput |
770 | + anchors.centerIn: parent |
771 | + |
772 | + width: root.width |
773 | + height: { |
774 | + if (fixedHeight) { |
775 | + return root.height; |
776 | + } |
777 | + var proposedHeight = mediaPlayer && mediaPlayer.metaData.resolution !== undefined ? |
778 | + (mediaPlayer.metaData.resolution.height / mediaPlayer.metaData.resolution.width) * width : |
779 | + image.height; |
780 | + if (maximumEmbeddedHeight !== undefined && maximumEmbeddedHeight < proposedHeight) { |
781 | + return maximumEmbeddedHeight; |
782 | + } |
783 | + return proposedHeight; |
784 | + } |
785 | + |
786 | + source: mediaPlayer |
787 | + visible: mediaPlayer && mediaPlayer.playbackState !== MediaPlayer.StoppedState || false |
788 | + |
789 | + Connections { |
790 | + target: mediaPlayer |
791 | + onError: { |
792 | + if (error !== MediaPlayer.NoError) { |
793 | + errorTimer.restart(); |
794 | + } |
795 | + } |
796 | + } |
797 | + } |
798 | + } |
799 | + |
800 | + Rectangle { |
801 | + id: playButton |
802 | + readonly property bool bigButton: parent.width > units.gu(40) |
803 | + anchors.centerIn: content |
804 | + width: bigButton ? units.gu(10) : units.gu(8) |
805 | + height: width |
806 | + visible: mediaPlayer && mediaPlayer.playbackState !== MediaPlayer.PlayingState || false |
807 | + color: "#1B1B1B" |
808 | + opacity: 0.85 |
809 | + radius: width/2 |
810 | + |
811 | + Behavior on width { UbuntuNumberAnimation {} } |
812 | + |
813 | + Icon { |
814 | + anchors.fill: parent |
815 | + anchors.margins: units.gu(1) |
816 | + name: errorTimer.running ? "dialog-warning-symbolic" : "media-playback-start" |
817 | + color: "#F3F3E7" |
818 | + } |
819 | + } |
820 | + |
821 | + ActivityIndicator { |
822 | + anchors.centerIn: content |
823 | + running: { |
824 | + if (!mediaPlayer) return false; |
825 | + return mediaPlayer.status === MediaPlayer.Stalled || |
826 | + (mediaPlayer.playbackState === MediaPlayer.PlayingState && mediaPlayer.status === MediaPlayer.Loading); |
827 | + } |
828 | + } |
829 | + |
830 | + MouseArea { |
831 | + id: contentMouseArea |
832 | + anchors.fill: content |
833 | + enabled: !errorTimer.running |
834 | + hoverEnabled: mediaPlayer && mediaPlayer.playbackState !== MediaPlayer.StoppedState || false |
835 | + |
836 | + onClicked: root.clicked() |
837 | + onPositionChanged: root.positionChanged() |
838 | + } |
839 | + |
840 | + Timer { |
841 | + id: errorTimer |
842 | + interval: 2000 |
843 | + } |
844 | +} |
845 | |
846 | === added file 'qml/Components/MediaServices/VideoPlayerControls.qml' |
847 | --- qml/Components/MediaServices/VideoPlayerControls.qml 1970-01-01 00:00:00 +0000 |
848 | +++ qml/Components/MediaServices/VideoPlayerControls.qml 2015-07-10 14:23:36 +0000 |
849 | @@ -0,0 +1,182 @@ |
850 | +/* |
851 | + * Copyright (C) 2015 Canonical, Ltd. |
852 | + * |
853 | + * This program is free software; you can redistribute it and/or modify |
854 | + * it under the terms of the GNU General Public License as published by |
855 | + * the Free Software Foundation; version 3. |
856 | + * |
857 | + * This program is distributed in the hope that it will be useful, |
858 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
859 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
860 | + * GNU General Public License for more details. |
861 | + * |
862 | + * You should have received a copy of the GNU General Public License |
863 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
864 | + */ |
865 | + |
866 | +import QtQuick 2.0 |
867 | +import QtQuick.Layouts 1.1 |
868 | +import QtMultimedia 5.0 |
869 | +import Ubuntu.Components 1.2 |
870 | + |
871 | +MediaServicesControls { |
872 | + id: root |
873 | + readonly property alias mediaPlayer: _mediaPlayer |
874 | + property bool interacting: false |
875 | + |
876 | + QtObject { |
877 | + id: priv |
878 | + |
879 | + function formatProgress(time) { |
880 | + time = Math.floor(time / 1000); |
881 | + |
882 | + var secs = time % 60; |
883 | + time = Math.floor(time / 60); |
884 | + var min = time % 60; |
885 | + var hour = Math.floor(time / 60); |
886 | + |
887 | + if (secs < 10) secs = "0%1".arg(secs); |
888 | + if (min < 10) min = "0%1".arg(min); |
889 | + if (hour > 0) { |
890 | + // TRANSLATORS: this refers to a duration/remaining time of the video in hours, minutes and seconds, |
891 | + // of which you can change the order. |
892 | + // %1 refers to hours, %2 refers to minutes and %3 refers to seconds. |
893 | + return i18n.tr("%1:%2:%3").arg(hour).arg(min).arg(secs); |
894 | + } else { |
895 | + // TRANSLATORS: this refers to a duration/remaining time of the video in minutes and seconds, |
896 | + // of which you can change the order. |
897 | + // %1 refers to minutes and %2 refers to seconds. |
898 | + return i18n.tr("%1:%2").arg(min).arg(secs); |
899 | + } |
900 | + } |
901 | + } |
902 | + |
903 | + component: Item { |
904 | + Connections { |
905 | + target: mediaPlayer |
906 | + onPositionChanged: { |
907 | + if (slider.valueGuard) return; |
908 | + |
909 | + slider.valueGuard = true; |
910 | + slider.value = mediaPlayer.position; |
911 | + slider.valueGuard = false; |
912 | + if (!slider.pressed) { |
913 | + positionLabel.text = priv.formatProgress(mediaPlayer.position); |
914 | + } |
915 | + } |
916 | + } |
917 | + |
918 | + Binding { |
919 | + target: root |
920 | + property: "interacting" |
921 | + value: slider.pressed |
922 | + } |
923 | + |
924 | + Label { |
925 | + id: positionLabel |
926 | + anchors { |
927 | + left: parent.left |
928 | + bottom: parent.bottom |
929 | + bottomMargin: -units.dp(3) |
930 | + } |
931 | + verticalAlignment: Text.AlignBottom |
932 | + fontSize: "x-small" |
933 | + color: "#F3F3E7" |
934 | + |
935 | + text: priv.formatProgress(mediaPlayer.position) |
936 | + } |
937 | + |
938 | + Slider { |
939 | + id: slider |
940 | + property bool valueGuard: false |
941 | + |
942 | + anchors { |
943 | + left: parent.left |
944 | + right: parent.right |
945 | + } |
946 | + height: units.gu(2) |
947 | + live: true |
948 | + enabled: mediaPlayer.seekable && mediaPlayer.duration > 0 |
949 | + minimumValue: 0 |
950 | + maximumValue: mediaPlayer.duration > 0 ? mediaPlayer.duration : 1 |
951 | + value: mediaPlayer.position |
952 | + |
953 | + onStyleInstanceChanged: { |
954 | + if (__styleInstance) __styleInstance.backgroundColor = "#F3F3E7"; |
955 | + } |
956 | + |
957 | + onValueChanged: { |
958 | + if (!pressed) return; |
959 | + if (slider.valueGuard) return; |
960 | + |
961 | + slider.valueGuard = true; |
962 | + mediaPlayer.seek(value); |
963 | + slider.valueGuard = false; |
964 | + } |
965 | + |
966 | + property bool wasPlaying: mediaPlayer.playbackState === MediaPlayer.PlayingState |
967 | + onPressedChanged: { |
968 | + if (pressed) { |
969 | + wasPlaying = mediaPlayer.playbackState === MediaPlayer.PlayingState |
970 | + mediaPlayer.pause(); |
971 | + } else { |
972 | + |
973 | + positionLabel.text = priv.formatProgress(mediaPlayer.position); |
974 | + if (wasPlaying) { |
975 | + mediaPlayer.play(); |
976 | + } |
977 | + } |
978 | + } |
979 | + |
980 | + function formatValue(value) { |
981 | + return priv.formatProgress(value); |
982 | + } |
983 | + } |
984 | + |
985 | + Label { |
986 | + anchors { |
987 | + right: parent.right |
988 | + bottom: parent.bottom |
989 | + bottomMargin: -units.dp(3) |
990 | + } |
991 | + verticalAlignment: Text.AlignBottom |
992 | + fontSize: "x-small" |
993 | + color: "#F3F3E7" |
994 | + |
995 | + text: priv.formatProgress(mediaPlayer.duration) |
996 | + } |
997 | + } |
998 | + |
999 | + function getPlaybackState(playbackState) { |
1000 | + if (playbackState === MediaPlayer.PlayingState) return "PlayingState"; |
1001 | + else if (playbackState === MediaPlayer.PausedState) return "PausedState"; |
1002 | + else if (playbackState === MediaPlayer.StoppedState) return "StoppedState"; |
1003 | + return ""; |
1004 | + } |
1005 | + |
1006 | + function getPlayerStatus(status) { |
1007 | + if (status === MediaPlayer.NoMedia) return "NoMedia"; |
1008 | + else if (status === MediaPlayer.Loading) return "Loading"; |
1009 | + else if (status === MediaPlayer.Loaded) return "Loaded"; |
1010 | + else if (status === MediaPlayer.Buffering) return "Buffering"; |
1011 | + else if (status === MediaPlayer.Stalled) return "Stalled"; |
1012 | + else if (status === MediaPlayer.Buffered) return "Buffered"; |
1013 | + else if (status === MediaPlayer.EndOfMedia) return "EndOfMedia"; |
1014 | + else if (status === MediaPlayer.InvalidMedia) return "InvalidMedia"; |
1015 | + else if (status === MediaPlayer.UnknownStatus) return "UnknownStatus"; |
1016 | + return ""; |
1017 | + } |
1018 | + |
1019 | + MediaPlayer { |
1020 | + id: _mediaPlayer |
1021 | + objectName: "mediaPlayer" |
1022 | + |
1023 | + onError: { |
1024 | + if (error !== MediaPlayer.NoError) { |
1025 | + stop(); |
1026 | + } |
1027 | + } |
1028 | +// onPlaybackStateChanged: console.log("PLAYBACK STATE CHANGED", getPlaybackState(playbackState)); |
1029 | +// onStatusChanged: console.log("PLAYER STATUS CHANGED", getPlayerStatus(status)); |
1030 | + } |
1031 | +} |
1032 | |
1033 | === modified file 'qml/Dash/GenericScopeView.qml' |
1034 | --- qml/Dash/GenericScopeView.qml 2015-07-03 14:24:08 +0000 |
1035 | +++ qml/Dash/GenericScopeView.qml 2015-07-10 14:23:36 +0000 |
1036 | @@ -762,6 +762,7 @@ |
1037 | item.open = Qt.binding(function() { return subPageLoader.open; } ) |
1038 | item.initialIndex = Qt.binding(function() { return subPageLoader.initialIndex; } ) |
1039 | item.model = Qt.binding(function() { return subPageLoader.model; } ) |
1040 | + item.currentIndex = subPageLoader.initialIndex; |
1041 | } |
1042 | open = true; |
1043 | } |
1044 | |
1045 | === added file 'qml/Dash/Previews/PreviewInlineVideo.qml' |
1046 | --- qml/Dash/Previews/PreviewInlineVideo.qml 1970-01-01 00:00:00 +0000 |
1047 | +++ qml/Dash/Previews/PreviewInlineVideo.qml 2015-07-10 14:23:36 +0000 |
1048 | @@ -0,0 +1,89 @@ |
1049 | +/* |
1050 | + * Copyright (C) 2015 Canonical, Ltd. |
1051 | + * |
1052 | + * This program is free software; you can redistribute it and/or modify |
1053 | + * it under the terms of the GNU General Public License as published by |
1054 | + * the Free Software Foundation; version 3. |
1055 | + * |
1056 | + * This program is distributed in the hope that it will be useful, |
1057 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
1058 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1059 | + * GNU General Public License for more details. |
1060 | + * |
1061 | + * You should have received a copy of the GNU General Public License |
1062 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
1063 | + */ |
1064 | + |
1065 | +import QtQuick 2.0 |
1066 | +import QtMultimedia 5.0 |
1067 | +import Ubuntu.Components 1.2 |
1068 | +import Ubuntu.Thumbnailer 0.1 |
1069 | +//import Ubuntu.Content 0.1 |
1070 | +import "../../Components/MediaServices" |
1071 | + |
1072 | +/*! \brief Preview widget for video. |
1073 | + |
1074 | + This widget shows video contained in widgetData["source"], |
1075 | + with a placeholder screenshot specified by widgetData["screenshot"]. |
1076 | + */ |
1077 | + |
1078 | +PreviewWidget { |
1079 | + id: root |
1080 | + implicitWidth: units.gu(35) |
1081 | + implicitHeight: services.height |
1082 | + |
1083 | + property alias rootItem: services.rootItem |
1084 | + |
1085 | + MediaServices { |
1086 | + id: services |
1087 | + width: parent.width |
1088 | + |
1089 | + context: "video" |
1090 | + sourceData: root.isCurrentPreview ? widgetData : undefined |
1091 | + fullscreen: false |
1092 | + maximumEmbeddedHeight: rootItem.height / 2 |
1093 | + |
1094 | + onClose: fullscreen = false |
1095 | + |
1096 | +// actions: [ |
1097 | +// Action { |
1098 | +// text: i18n.tr("Share") |
1099 | +// iconSource: "image://theme/share" |
1100 | +// onTriggered: sharePicker.visible = true |
1101 | +// } |
1102 | +// ] |
1103 | + } |
1104 | + |
1105 | +// Component { |
1106 | +// id: contentItemComp |
1107 | +// ContentItem { |
1108 | +// url: widgetData["source"] |
1109 | +// } |
1110 | +// } |
1111 | +// ContentPeerPicker { |
1112 | +// id: sharePicker |
1113 | +// objectName: "sharePickerEvents" |
1114 | +// anchors.fill: parent |
1115 | +// showTitle: false |
1116 | +// visible: false |
1117 | +// parent: rootItem |
1118 | +// z: 100 |
1119 | + |
1120 | +// contentType: ContentType.Videos |
1121 | +// handler: ContentHandler.Share |
1122 | + |
1123 | +// onPeerSelected: { |
1124 | +// visible = false; |
1125 | + |
1126 | +// var curTransfer = peer.request(); |
1127 | +// if (curTransfer.state === ContentTransfer.InProgress) |
1128 | +// { |
1129 | +// var medias = [ contentItemComp.createObject(parent) ] |
1130 | +// curTransfer.state = ContentTransfer.Charged; |
1131 | +// } |
1132 | +// } |
1133 | +// onCancelPressed: { |
1134 | +// visible = false; |
1135 | +// } |
1136 | +// } |
1137 | +} |
1138 | |
1139 | === modified file 'qml/Dash/Previews/PreviewWidgetFactory.qml' |
1140 | --- qml/Dash/Previews/PreviewWidgetFactory.qml 2015-05-08 13:12:02 +0000 |
1141 | +++ qml/Dash/Previews/PreviewWidgetFactory.qml 2015-07-10 14:23:36 +0000 |
1142 | @@ -61,7 +61,14 @@ |
1143 | case "reviews": return "PreviewRatingDisplay.qml"; |
1144 | case "table": return "PreviewTable.qml"; |
1145 | case "text": return "PreviewTextSummary.qml"; |
1146 | - case "video": return "PreviewVideoPlayback.qml"; |
1147 | + case "video": { |
1148 | + if (!widgetData) return ""; |
1149 | + var source = widgetData.hasOwnProperty("source") ? widgetData["source"].toString() : ""; |
1150 | + if (source.match("^http") !== null) { |
1151 | + return "PreviewVideoPlayback.qml"; |
1152 | + } |
1153 | + return "PreviewInlineVideo.qml"; |
1154 | + } |
1155 | default: return ""; |
1156 | } |
1157 | } |
1158 | |
1159 | === modified file 'tests/mocks/QtMultimedia/CMakeLists.txt' |
1160 | --- tests/mocks/QtMultimedia/CMakeLists.txt 2014-05-02 22:57:21 +0000 |
1161 | +++ tests/mocks/QtMultimedia/CMakeLists.txt 2015-07-10 14:23:36 +0000 |
1162 | @@ -1,8 +1,14 @@ |
1163 | +include_directories( |
1164 | + ${CMAKE_CURRENT_SOURCE_DIR} |
1165 | + ${CMAKE_CURRENT_BINARY_DIR} |
1166 | +) |
1167 | + |
1168 | add_library(QtMultimedia-qml MODULE |
1169 | plugin.cpp |
1170 | - audio.cpp |
1171 | + mediaplayer.cpp |
1172 | + videooutput.cpp |
1173 | ) |
1174 | |
1175 | -qt5_use_modules(QtMultimedia-qml Qml) |
1176 | +qt5_use_modules(QtMultimedia-qml Qml Quick) |
1177 | |
1178 | add_unity8_mock(QtMultimedia 5.0 QtMultimedia TARGETS QtMultimedia-qml) |
1179 | |
1180 | === added file 'tests/mocks/QtMultimedia/VideoSurface.qml' |
1181 | --- tests/mocks/QtMultimedia/VideoSurface.qml 1970-01-01 00:00:00 +0000 |
1182 | +++ tests/mocks/QtMultimedia/VideoSurface.qml 2015-07-10 14:23:36 +0000 |
1183 | @@ -0,0 +1,111 @@ |
1184 | +/* |
1185 | + * Copyright (C) 2015 Canonical, Ltd. |
1186 | + * |
1187 | + * This program is free software; you can redistribute it and/or modify |
1188 | + * it under the terms of the GNU General Public License as published by |
1189 | + * the Free Software Foundation; version 3. |
1190 | + * |
1191 | + * This program is distributed in the hope that it will be useful, |
1192 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
1193 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1194 | + * GNU General Public License for more details. |
1195 | + * |
1196 | + * You should have received a copy of the GNU General Public License |
1197 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
1198 | + */ |
1199 | + |
1200 | +import QtQuick 2.0 |
1201 | +import Ubuntu.Components 1.2 |
1202 | +import QtMultimedia 5.0 |
1203 | + |
1204 | +Rectangle { |
1205 | + id: root |
1206 | + property int position: 0 |
1207 | + property int playbackState: 0 |
1208 | + property var resolution: { "width": 16, "height": 9 } |
1209 | + |
1210 | + anchors.centerIn: parent |
1211 | + color: "black" |
1212 | + |
1213 | + // scale to the parent window. |
1214 | + implicitWidth: parent ? aspectScale(Qt.size(resolution.width, resolution.height), Qt.size(parent.width, parent.height)).width : 160 |
1215 | + implicitHeight: parent ? aspectScale(Qt.size(resolution.width, resolution.height), Qt.size(parent.width, parent.height)).height : 90 |
1216 | + |
1217 | + function aspectScale(fromSize, toSize) { |
1218 | + var rw = toSize.height * fromSize.width / fromSize.height; |
1219 | + var useHeight = (rw <= toSize.width); |
1220 | + |
1221 | + if (useHeight) { |
1222 | + return Qt.size(rw, toSize.height); |
1223 | + } else { |
1224 | + return Qt.size(toSize.width, |
1225 | + toSize.width * (fromSize.height / fromSize.width)); |
1226 | + } |
1227 | + } |
1228 | + |
1229 | + GridView { |
1230 | + anchors.fill: parent |
1231 | + model: (parent.width / units.gu(5)) * ((parent.height / units.gu(5) + 1)) |
1232 | + clip: true |
1233 | + |
1234 | + cellHeight: units.gu(5) |
1235 | + cellWidth: units.gu(5) |
1236 | + |
1237 | + delegate: Item { |
1238 | + width: units.gu(5) |
1239 | + height: units.gu(5) |
1240 | + |
1241 | + Rectangle { |
1242 | + id: rect |
1243 | + color: "red" |
1244 | + anchors.fill: parent |
1245 | + anchors.margins: units.gu(1) |
1246 | + |
1247 | + Connections { |
1248 | + target: root |
1249 | + onPositionChanged: { |
1250 | + rect.rotation = position/100 |
1251 | + } |
1252 | + } |
1253 | + } |
1254 | + } |
1255 | + } |
1256 | + |
1257 | + Column { |
1258 | + anchors { |
1259 | + left: parent.left; |
1260 | + right: parent.right |
1261 | + verticalCenter: parent.verticalCenter |
1262 | + } |
1263 | + height: units.gu(10) |
1264 | + |
1265 | + Label { |
1266 | + anchors.horizontalCenter: parent.horizontalCenter |
1267 | + text: { |
1268 | + var pos = (position/1000).toFixed(0) |
1269 | + |
1270 | + var m = Math.floor(pos/60); |
1271 | + var ss = pos % 60; |
1272 | + if (ss >= 10) { |
1273 | + return m + ":" + ss; |
1274 | + } else { |
1275 | + return m + ":0" + ss; |
1276 | + } |
1277 | + } |
1278 | + fontSize: "x-large" |
1279 | + } |
1280 | + |
1281 | + Label { |
1282 | + anchors.horizontalCenter: parent.horizontalCenter |
1283 | + text: getPlaybackState(playbackState) |
1284 | + fontSize: "x-large" |
1285 | + } |
1286 | + } |
1287 | + |
1288 | + function getPlaybackState(playbackState) { |
1289 | + if (playbackState === MediaPlayer.PlayingState) return "Playing"; |
1290 | + else if (playbackState === MediaPlayer.PausedState) return "Paused"; |
1291 | + else if (playbackState === MediaPlayer.StoppedState) return "Stopped"; |
1292 | + return ""; |
1293 | + } |
1294 | +} |
1295 | |
1296 | === renamed file 'tests/mocks/QtMultimedia/audio.cpp' => 'tests/mocks/QtMultimedia/mediaplayer.cpp' |
1297 | --- tests/mocks/QtMultimedia/audio.cpp 2015-06-08 11:37:27 +0000 |
1298 | +++ tests/mocks/QtMultimedia/mediaplayer.cpp 2015-07-10 14:23:36 +0000 |
1299 | @@ -16,57 +16,123 @@ |
1300 | * Author: Michael Zanetti <michael.zanetti@canonical.com> |
1301 | */ |
1302 | |
1303 | -#include "audio.h" |
1304 | - |
1305 | -Audio::Audio(QObject* parent): |
1306 | - QObject(parent), |
1307 | - m_playbackState(StoppedState) |
1308 | +#include "mediaplayer.h" |
1309 | + |
1310 | +#include <QDebug> |
1311 | +#include <QSize> |
1312 | + |
1313 | +class MetaDataObject: public QObject |
1314 | +{ |
1315 | + Q_OBJECT |
1316 | + Q_PROPERTY(QVariant title READ title NOTIFY metaDataChanged) |
1317 | + Q_PROPERTY(QVariant resolution READ resolution NOTIFY metaDataChanged) |
1318 | +public: |
1319 | + MetaDataObject(MediaPlayer* parent = nullptr) |
1320 | + : QObject(parent) |
1321 | + , m_source(parent) |
1322 | + { |
1323 | + } |
1324 | + |
1325 | + QVariant title() const { return getProperty("title"); } |
1326 | + QVariant resolution() const { return getProperty("resolution", QSize(640, 640)); } |
1327 | + |
1328 | + QVariant getProperty(const QString& property, QVariant defaultValue = QVariant()) const { |
1329 | + MediaDataSource* sourceData = MediaPlayerDataController::instance()->dataForSource(m_source->source()); |
1330 | + QVariant metaData = sourceData->metaData(); |
1331 | + return metaData.toMap().value(property, defaultValue); |
1332 | + } |
1333 | + |
1334 | +Q_SIGNALS: |
1335 | + void metaDataChanged(); |
1336 | + |
1337 | +private: |
1338 | + MediaPlayer* m_source; |
1339 | +}; |
1340 | + |
1341 | +MediaPlayer::MediaPlayer(QObject* parent) |
1342 | + : QObject(parent) |
1343 | + , m_playbackState(StoppedState) |
1344 | + , m_status(NoMedia) |
1345 | + , m_metaData(new MetaDataObject(this)) |
1346 | { |
1347 | qsrand(time(nullptr)); |
1348 | - m_timer.setInterval(1000); |
1349 | + m_timer.setInterval(100); |
1350 | connect(&m_timer, SIGNAL(timeout()), SLOT(timerEvent())); |
1351 | + |
1352 | + connect(MediaPlayerDataController::instance(), &MediaPlayerDataController::sourceAboutToBeRemoved, |
1353 | + this, [this](const QUrl& source) { |
1354 | + MediaDataSource* dataSource = MediaPlayerDataController::instance()->dataForSource(source); |
1355 | + if (!dataSource) return; |
1356 | + |
1357 | + disconnect(dataSource, &MediaDataSource::durationChanged, this, &MediaPlayer::durationChanged); |
1358 | + disconnect(dataSource, &MediaDataSource::seekableChanged, this, &MediaPlayer::seekableChanged); |
1359 | + disconnect(dataSource, &MediaDataSource::availabilityChanged, this, &MediaPlayer::availabilityChanged); |
1360 | + disconnect(dataSource, &MediaDataSource::metaDataChanged, m_metaData, &MetaDataObject::metaDataChanged); |
1361 | + }); |
1362 | + |
1363 | + connect(MediaPlayerDataController::instance(), &MediaPlayerDataController::sourceAdded, |
1364 | + this, [this](const QUrl& source) { |
1365 | + MediaDataSource* dataSource = MediaPlayerDataController::instance()->dataForSource(source); |
1366 | + if (!dataSource) return; |
1367 | + |
1368 | + connect(dataSource, &MediaDataSource::durationChanged, this, &MediaPlayer::durationChanged); |
1369 | + connect(dataSource, &MediaDataSource::seekableChanged,this, &MediaPlayer::seekableChanged); |
1370 | + connect(dataSource, &MediaDataSource::availabilityChanged,this, &MediaPlayer::availabilityChanged); |
1371 | + connect(dataSource, &MediaDataSource::metaDataChanged, m_metaData, &MetaDataObject::metaDataChanged); |
1372 | + }); |
1373 | } |
1374 | |
1375 | -QUrl Audio::source() const |
1376 | +QUrl MediaPlayer::source() const |
1377 | { |
1378 | return m_source; |
1379 | } |
1380 | |
1381 | -void Audio::setSource(const QUrl &source) |
1382 | +void MediaPlayer::setSource(const QUrl &source) |
1383 | { |
1384 | if (m_source != source) { |
1385 | m_source = source; |
1386 | Q_EMIT sourceChanged(source); |
1387 | + Q_EMIT durationChanged(duration()); |
1388 | + Q_EMIT seekableChanged(isSeekable()); |
1389 | + Q_EMIT availabilityChanged(availability()); |
1390 | |
1391 | m_position = 0; |
1392 | Q_EMIT positionChanged(m_position); |
1393 | + Q_EMIT m_metaData->metaDataChanged(); |
1394 | |
1395 | - m_duration = (qrand() % 20000) + 10000; |
1396 | - Q_EMIT durationChanged(m_duration); |
1397 | + m_status = availability()==Available ? Loaded : InvalidMedia; |
1398 | + Q_EMIT statusChanged(); |
1399 | } |
1400 | } |
1401 | |
1402 | -Audio::PlaybackState Audio::playbackState() const |
1403 | +MediaPlayer::PlaybackState MediaPlayer::playbackState() const |
1404 | { |
1405 | return m_playbackState; |
1406 | } |
1407 | |
1408 | -int Audio::position() const |
1409 | +int MediaPlayer::position() const |
1410 | { |
1411 | return m_position; |
1412 | } |
1413 | |
1414 | -int Audio::duration() const |
1415 | -{ |
1416 | - return m_duration; |
1417 | -} |
1418 | - |
1419 | -QString Audio::errorString() const |
1420 | +int MediaPlayer::duration() const |
1421 | +{ |
1422 | + MediaDataSource* dataSource = MediaPlayerDataController::instance()->dataForSource(source()); |
1423 | + if (dataSource) return dataSource->duration(); |
1424 | + return 0; |
1425 | +} |
1426 | + |
1427 | +MediaPlayer::Error MediaPlayer::error() const |
1428 | +{ |
1429 | + return NoError; |
1430 | +} |
1431 | + |
1432 | +QString MediaPlayer::errorString() const |
1433 | { |
1434 | return QString(); |
1435 | } |
1436 | |
1437 | -void Audio::pause() |
1438 | +void MediaPlayer::pause() |
1439 | { |
1440 | if (m_playbackState == PlayingState) { |
1441 | m_playbackState = PausedState; |
1442 | @@ -75,7 +141,7 @@ |
1443 | } |
1444 | } |
1445 | |
1446 | -void Audio::play() |
1447 | +void MediaPlayer::play() |
1448 | { |
1449 | if (m_playbackState != PlayingState) { |
1450 | m_playbackState = PlayingState; |
1451 | @@ -85,7 +151,7 @@ |
1452 | } |
1453 | } |
1454 | |
1455 | -void Audio::stop() |
1456 | +void MediaPlayer::stop() |
1457 | { |
1458 | if (m_playbackState != StoppedState) { |
1459 | m_playbackState = StoppedState; |
1460 | @@ -96,22 +162,143 @@ |
1461 | } |
1462 | } |
1463 | |
1464 | -void Audio::timerEvent() |
1465 | -{ |
1466 | - if (m_position + 1000 < m_duration) { |
1467 | - m_position += 1000; |
1468 | +void MediaPlayer::seek(int position) |
1469 | +{ |
1470 | + if (status() != Loaded) return; |
1471 | + int newPosition = qMin(qMax(0, position), duration()); |
1472 | + if (newPosition != m_position) { |
1473 | + m_position = newPosition; |
1474 | + Q_EMIT positionChanged(m_position); |
1475 | + } |
1476 | + |
1477 | +} |
1478 | + |
1479 | +void MediaPlayer::timerEvent() |
1480 | +{ |
1481 | + if (m_position + m_timer.interval() < duration()) { |
1482 | + m_position += m_timer.interval(); |
1483 | Q_EMIT positionChanged(m_position); |
1484 | } else { |
1485 | - stop(); |
1486 | + pause(); |
1487 | } |
1488 | } |
1489 | |
1490 | -Audio::AudioRole Audio::audioRole() const |
1491 | +MediaPlayer::AudioRole MediaPlayer::audioRole() const |
1492 | { |
1493 | - return Audio::multimedia; |
1494 | + return MediaPlayer::multimedia; |
1495 | } |
1496 | |
1497 | -void Audio::setAudioRole(Audio::AudioRole audioRole) |
1498 | +void MediaPlayer::setAudioRole(MediaPlayer::AudioRole audioRole) |
1499 | { |
1500 | Q_UNUSED(audioRole); |
1501 | } |
1502 | + |
1503 | +bool MediaPlayer::isSeekable() const |
1504 | +{ |
1505 | + MediaDataSource* dataSource = MediaPlayerDataController::instance()->dataForSource(source()); |
1506 | + if (dataSource) return dataSource->isSeekable(); |
1507 | + return true; |
1508 | +} |
1509 | + |
1510 | +MediaPlayer::Availability MediaPlayer::availability() const |
1511 | +{ |
1512 | + MediaDataSource* dataSource = MediaPlayerDataController::instance()->dataForSource(source()); |
1513 | + if (dataSource) return dataSource->availability(); |
1514 | + return Available; |
1515 | +} |
1516 | + |
1517 | +QObject *MediaPlayer::metaData() const |
1518 | +{ |
1519 | + return m_metaData; |
1520 | +} |
1521 | + |
1522 | + |
1523 | +MediaDataSource::MediaDataSource(QObject *parent) |
1524 | + : QObject(parent) |
1525 | + , m_seekable(true) |
1526 | + , m_duration((qrand() % 20000) + 10000) |
1527 | + , m_availability(MediaPlayer::Available) |
1528 | +{ |
1529 | +} |
1530 | + |
1531 | +MediaDataSource::~MediaDataSource() |
1532 | +{ |
1533 | + if (!m_source.isEmpty()) MediaPlayerDataController::instance()->unregisterDataSource(this); |
1534 | +} |
1535 | + |
1536 | +void MediaDataSource::setSource(const QUrl &source) |
1537 | +{ |
1538 | + if (m_source != source) { |
1539 | + if (!m_source.isEmpty()) MediaPlayerDataController::instance()->unregisterDataSource(this); |
1540 | + |
1541 | + m_source = source; |
1542 | + |
1543 | + if (!m_source.isEmpty()) MediaPlayerDataController::instance()->registerDataSource(this); |
1544 | + Q_EMIT sourceChanged(); |
1545 | + } |
1546 | +} |
1547 | + |
1548 | +void MediaDataSource::setSeekable(bool seekable) |
1549 | +{ |
1550 | + if (m_seekable != seekable) { |
1551 | + m_seekable = seekable; |
1552 | + Q_EMIT seekableChanged(m_seekable); |
1553 | + } |
1554 | +} |
1555 | + |
1556 | +void MediaDataSource::setDuration(int duration) |
1557 | +{ |
1558 | + if (m_duration != duration) { |
1559 | + m_duration = duration; |
1560 | + Q_EMIT durationChanged(m_duration); |
1561 | + } |
1562 | +} |
1563 | + |
1564 | +void MediaDataSource::setAvailability(MediaPlayer::Availability availability) |
1565 | +{ |
1566 | + if (m_availability != availability) { |
1567 | + m_availability = availability; |
1568 | + Q_EMIT availabilityChanged(m_availability); |
1569 | + } |
1570 | +} |
1571 | + |
1572 | +void MediaDataSource::setMetaData(const QVariant& metaData) |
1573 | +{ |
1574 | + if (m_metaData != metaData) { |
1575 | + m_metaData = metaData; |
1576 | + Q_EMIT metaDataChanged(); |
1577 | + } |
1578 | +} |
1579 | + |
1580 | +MediaPlayerDataController *MediaPlayerDataController::instance() |
1581 | +{ |
1582 | + static MediaPlayerDataController* instance = nullptr; |
1583 | + if (!instance) { instance = new MediaPlayerDataController(); } |
1584 | + return instance; |
1585 | +} |
1586 | + |
1587 | +void MediaPlayerDataController::registerDataSource(MediaDataSource *dataSource) |
1588 | +{ |
1589 | + m_dataSources[dataSource->source()] = dataSource; |
1590 | + Q_EMIT sourceAdded(dataSource->source()); |
1591 | +} |
1592 | + |
1593 | +void MediaPlayerDataController::unregisterDataSource(MediaDataSource *dataSource) |
1594 | +{ |
1595 | + QList<QUrl> keys = m_dataSources.keys(dataSource); |
1596 | + Q_FOREACH(const QUrl& key, keys) { |
1597 | + Q_EMIT sourceAboutToBeRemoved(key); |
1598 | + m_dataSources.remove(key); |
1599 | + } |
1600 | +} |
1601 | + |
1602 | +MediaDataSource *MediaPlayerDataController::dataForSource(const QUrl &source) |
1603 | +{ |
1604 | + if (!m_dataSources.contains(source)) { |
1605 | + static MediaDataSource defaultSource; |
1606 | + return &defaultSource; |
1607 | + } |
1608 | + return m_dataSources[source]; |
1609 | +} |
1610 | + |
1611 | +#include "mediaplayer.moc" |
1612 | |
1613 | === renamed file 'tests/mocks/QtMultimedia/audio.h' => 'tests/mocks/QtMultimedia/mediaplayer.h' |
1614 | --- tests/mocks/QtMultimedia/audio.h 2015-06-08 11:37:27 +0000 |
1615 | +++ tests/mocks/QtMultimedia/mediaplayer.h 2015-07-10 14:23:36 +0000 |
1616 | @@ -16,24 +16,39 @@ |
1617 | * Authors: Michael Zanetti <michael.zanetti@canonical.com> |
1618 | */ |
1619 | |
1620 | -#ifndef MOCK_AUDIO_H |
1621 | -#define MOCK_AUDIO_H |
1622 | +#ifndef MOCK_MEDIAPLAYER_H |
1623 | +#define MOCK_MEDIAPLAYER_H |
1624 | |
1625 | #include <QObject> |
1626 | #include <QUrl> |
1627 | #include <QTimer> |
1628 | - |
1629 | -class Audio: public QObject |
1630 | +#include <QHash> |
1631 | +#include <QVariant> |
1632 | + |
1633 | +class MetaDataObject; |
1634 | + |
1635 | +class MediaPlayer: public QObject |
1636 | { |
1637 | Q_OBJECT |
1638 | - Q_ENUMS(PlaybackState) |
1639 | - Q_ENUMS(AudioRole) |
1640 | + |
1641 | Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged) |
1642 | Q_PROPERTY(PlaybackState playbackState READ playbackState NOTIFY playbackStateChanged) |
1643 | Q_PROPERTY(int position READ position NOTIFY positionChanged) |
1644 | Q_PROPERTY(int duration READ duration NOTIFY durationChanged) |
1645 | + Q_PROPERTY(Error error READ error NOTIFY errorChanged) |
1646 | Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged) |
1647 | Q_PROPERTY(AudioRole audioRole READ audioRole WRITE setAudioRole) |
1648 | + |
1649 | + Q_PROPERTY(bool seekable READ isSeekable NOTIFY seekableChanged) |
1650 | + Q_PROPERTY(Availability availability READ availability NOTIFY availabilityChanged) |
1651 | + Q_PROPERTY(Status status READ status NOTIFY statusChanged) |
1652 | + Q_PROPERTY(QObject *metaData READ metaData CONSTANT) |
1653 | + |
1654 | + Q_ENUMS(PlaybackState) |
1655 | + Q_ENUMS(AudioRole) |
1656 | + Q_ENUMS(Availability) |
1657 | + Q_ENUMS(Status) |
1658 | + Q_ENUMS(Error) |
1659 | public: |
1660 | enum PlaybackState { |
1661 | PlayingState, |
1662 | @@ -48,7 +63,35 @@ |
1663 | phone |
1664 | }; |
1665 | |
1666 | - explicit Audio(QObject *parent = 0); |
1667 | + enum Availability { |
1668 | + Available, |
1669 | + Busy, |
1670 | + Unavailable, |
1671 | + ResourceMissing |
1672 | + }; |
1673 | + |
1674 | + enum Status { |
1675 | + UnknownStatus, |
1676 | + NoMedia, |
1677 | + Loading, |
1678 | + Loaded, |
1679 | + Stalled, |
1680 | + Buffering, |
1681 | + Buffered, |
1682 | + EndOfMedia, |
1683 | + InvalidMedia |
1684 | + }; |
1685 | + |
1686 | + enum Error { |
1687 | + NoError, |
1688 | + ResourceError, |
1689 | + FormatError, |
1690 | + NetworkError, |
1691 | + AccessDeniedError, |
1692 | + ServiceMissingError |
1693 | + }; |
1694 | + |
1695 | + explicit MediaPlayer(QObject *parent = 0); |
1696 | |
1697 | QUrl source() const; |
1698 | void setSource(const QUrl &source); |
1699 | @@ -59,22 +102,35 @@ |
1700 | |
1701 | int duration() const; |
1702 | |
1703 | + Error error() const; |
1704 | QString errorString() const; |
1705 | |
1706 | AudioRole audioRole() const; |
1707 | void setAudioRole(AudioRole audioRole); |
1708 | |
1709 | + bool isSeekable() const; |
1710 | + MediaPlayer::Availability availability() const; |
1711 | + Status status() const { return m_status; } |
1712 | + QObject *metaData() const; |
1713 | + |
1714 | public Q_SLOTS: |
1715 | void pause(); |
1716 | void play(); |
1717 | void stop(); |
1718 | + void seek(int position); |
1719 | |
1720 | Q_SIGNALS: |
1721 | void sourceChanged(const QUrl &source); |
1722 | void playbackStateChanged(PlaybackState playbackState); |
1723 | void positionChanged(int position); |
1724 | void durationChanged(int duration); |
1725 | + void seekableChanged(bool seekable); |
1726 | + void errorChanged(Error error); |
1727 | void errorStringChanged(const QString &errorString); |
1728 | + void availabilityChanged(Availability availability); |
1729 | + void statusChanged(); |
1730 | + |
1731 | + void error(Error error, const QString &errorString); |
1732 | |
1733 | private Q_SLOTS: |
1734 | void timerEvent(); |
1735 | @@ -84,7 +140,72 @@ |
1736 | PlaybackState m_playbackState; |
1737 | QTimer m_timer; |
1738 | int m_position; |
1739 | + Status m_status; |
1740 | + MetaDataObject *m_metaData; |
1741 | +}; |
1742 | + |
1743 | +class MediaDataSource : public QObject |
1744 | +{ |
1745 | + Q_OBJECT |
1746 | + Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged) |
1747 | + Q_PROPERTY(bool seekable READ isSeekable WRITE setSeekable NOTIFY seekableChanged) |
1748 | + Q_PROPERTY(int duration READ duration WRITE setDuration NOTIFY durationChanged) |
1749 | + Q_PROPERTY(MediaPlayer::Availability availability READ availability WRITE setAvailability NOTIFY availabilityChanged) |
1750 | + Q_PROPERTY(QVariant metaData READ metaData WRITE setMetaData NOTIFY metaDataChanged) |
1751 | + |
1752 | +public: |
1753 | + MediaDataSource(QObject* parent = 0); |
1754 | + ~MediaDataSource(); |
1755 | + |
1756 | + QUrl source() const { return m_source; } |
1757 | + void setSource(const QUrl& source); |
1758 | + |
1759 | + bool isSeekable() const { return m_seekable; } |
1760 | + void setSeekable(bool seekable); |
1761 | + |
1762 | + int duration() const { return m_duration; } |
1763 | + void setDuration(int duration); |
1764 | + |
1765 | + MediaPlayer::Availability availability() const { return m_availability; } |
1766 | + void setAvailability(MediaPlayer::Availability availability); |
1767 | + |
1768 | + QVariant metaData() const { return m_metaData; } |
1769 | + void setMetaData(const QVariant& metaData); |
1770 | + |
1771 | +Q_SIGNALS: |
1772 | + void sourceChanged(); |
1773 | + void seekableChanged(bool seekable); |
1774 | + void durationChanged(int duration); |
1775 | + void availabilityChanged(MediaPlayer::Availability availability); |
1776 | + void metaDataChanged(); |
1777 | + |
1778 | +private: |
1779 | + QUrl m_source; |
1780 | + bool m_seekable; |
1781 | int m_duration; |
1782 | -}; |
1783 | - |
1784 | -#endif |
1785 | + MediaPlayer::Availability m_availability; |
1786 | + QVariant m_metaData; |
1787 | +}; |
1788 | + |
1789 | +class MediaPlayerDataController : public QObject |
1790 | +{ |
1791 | + Q_OBJECT |
1792 | +public: |
1793 | + MediaPlayerDataController() = default; |
1794 | + |
1795 | + static MediaPlayerDataController *instance(); |
1796 | + |
1797 | + void registerDataSource(MediaDataSource* dataSource); |
1798 | + void unregisterDataSource(MediaDataSource* dataSource); |
1799 | + |
1800 | + MediaDataSource* dataForSource(const QUrl& source); |
1801 | + |
1802 | +Q_SIGNALS: |
1803 | + void sourceAdded(const QUrl& source); |
1804 | + void sourceAboutToBeRemoved(const QUrl& source); |
1805 | + |
1806 | +public: |
1807 | + QHash<QUrl, MediaDataSource*> m_dataSources; |
1808 | +}; |
1809 | + |
1810 | +#endif // MOCK_MEDIAPLAYER_H |
1811 | |
1812 | === modified file 'tests/mocks/QtMultimedia/plugin.cpp' |
1813 | --- tests/mocks/QtMultimedia/plugin.cpp 2015-06-08 11:37:27 +0000 |
1814 | +++ tests/mocks/QtMultimedia/plugin.cpp 2015-07-10 14:23:36 +0000 |
1815 | @@ -17,13 +17,17 @@ |
1816 | */ |
1817 | |
1818 | #include "plugin.h" |
1819 | -#include "audio.h" |
1820 | +#include "mediaplayer.h" |
1821 | +#include "videooutput.h" |
1822 | |
1823 | #include <QtQml/qqml.h> |
1824 | |
1825 | void MockQtMultimediaPlugin::registerTypes(const char *uri) |
1826 | { |
1827 | Q_ASSERT(uri == QLatin1String("QtMultimedia")); |
1828 | - qmlRegisterType<Audio>(uri, 5, 0, "Audio"); |
1829 | - qmlRegisterType<Audio>(uri, 5, 0, "MediaPlayer"); |
1830 | + qmlRegisterType<MediaPlayer>(uri, 5, 0, "Audio"); |
1831 | + qmlRegisterType<MediaPlayer>(uri, 5, 0, "MediaPlayer"); |
1832 | + qmlRegisterType<VideoOutput>(uri, 5, 0, "VideoOutput"); |
1833 | + |
1834 | + qmlRegisterType<MediaDataSource>(uri, 5, 0, "MediaDataSource"); |
1835 | } |
1836 | |
1837 | === added file 'tests/mocks/QtMultimedia/videooutput.cpp' |
1838 | --- tests/mocks/QtMultimedia/videooutput.cpp 1970-01-01 00:00:00 +0000 |
1839 | +++ tests/mocks/QtMultimedia/videooutput.cpp 2015-07-10 14:23:36 +0000 |
1840 | @@ -0,0 +1,127 @@ |
1841 | +/* |
1842 | + * Copyright (C) 2015 Canonical, Ltd. |
1843 | + * |
1844 | + * This program is free software; you can redistribute it and/or modify |
1845 | + * it under the terms of the GNU General Public License as published by |
1846 | + * the Free Software Foundation; version 3. |
1847 | + * |
1848 | + * This program is distributed in the hope that it will be useful, |
1849 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
1850 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1851 | + * GNU General Public License for more details. |
1852 | + * |
1853 | + * You should have received a copy of the GNU General Public License |
1854 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
1855 | + */ |
1856 | + |
1857 | +#include "videooutput.h" |
1858 | +#include "mediaplayer.h" |
1859 | + |
1860 | +#include <paths.h> |
1861 | + |
1862 | +#include <QQuickView> |
1863 | +#include <QQmlComponent> |
1864 | +#include <QGuiApplication> |
1865 | +#include <QQmlProperty> |
1866 | +#include <QQmlEngine> |
1867 | + |
1868 | +VideoOutput::VideoOutput(QQuickItem *parent) |
1869 | + : QQuickItem(parent) |
1870 | + , m_source(nullptr) |
1871 | + , m_qmlContentComponent(nullptr) |
1872 | + , m_qmlItem(nullptr) |
1873 | +{ |
1874 | +} |
1875 | + |
1876 | +void VideoOutput::setSource(QObject *source) |
1877 | +{ |
1878 | + if (m_source != source) { |
1879 | + if (m_source) disconnect(m_source, 0, this, 0); |
1880 | + |
1881 | + m_source = source; |
1882 | + Q_EMIT sourceChanged(); |
1883 | + |
1884 | + MediaPlayer* mediaPlayer = qobject_cast<MediaPlayer*>(source); |
1885 | + if (mediaPlayer) { |
1886 | + connect(mediaPlayer, &MediaPlayer::positionChanged, this, &VideoOutput::updateProperties); |
1887 | + connect(mediaPlayer, &MediaPlayer::playbackStateChanged, this, &VideoOutput::updateProperties); |
1888 | + connect(mediaPlayer->metaData(), SIGNAL(metaDataChanged()), this, SLOT(updateProperties())); |
1889 | + } |
1890 | + } |
1891 | +} |
1892 | + |
1893 | +void VideoOutput::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) |
1894 | +{ |
1895 | + if (change == QQuickItem::ItemSceneChange && !m_qmlContentComponent) { |
1896 | + QWindowList list = QGuiApplication::topLevelWindows(); |
1897 | + if (list.isEmpty()) return; |
1898 | + |
1899 | + // The assumptions I make here really should hold. |
1900 | + QQuickView *quickView = |
1901 | + qobject_cast<QQuickView*>(list[0]); |
1902 | + |
1903 | + m_qmlContentComponent = new QQmlComponent(quickView->engine(), |
1904 | + QString("%1/QtMultimedia/VideoSurface.qml").arg(mockPluginsDir())); |
1905 | + |
1906 | + switch (m_qmlContentComponent->status()) { |
1907 | + case QQmlComponent::Ready: |
1908 | + createQmlContentItem(); |
1909 | + break; |
1910 | + case QQmlComponent::Loading: |
1911 | + connect(m_qmlContentComponent, &QQmlComponent::statusChanged, |
1912 | + this, &VideoOutput::onComponentStatusChanged); |
1913 | + break; |
1914 | + case QQmlComponent::Error: |
1915 | + printComponentErrors(); |
1916 | + qFatal("VideoOutput: failed to create content component."); |
1917 | + break; |
1918 | + default: |
1919 | + qFatal("VideoOutput: Unhandled component status"); |
1920 | + } |
1921 | + } |
1922 | + QQuickItem::itemChange(change, value); |
1923 | +} |
1924 | + |
1925 | +void VideoOutput::printComponentErrors() |
1926 | +{ |
1927 | + QList<QQmlError> errors = m_qmlContentComponent->errors(); |
1928 | + for (int i = 0; i < errors.count(); ++i) { |
1929 | + qDebug() << errors[i]; |
1930 | + } |
1931 | +} |
1932 | + |
1933 | +void VideoOutput::onComponentStatusChanged(QQmlComponent::Status status) |
1934 | +{ |
1935 | + if (status == QQmlComponent::Ready) { |
1936 | + createQmlContentItem(); |
1937 | + } |
1938 | +} |
1939 | + |
1940 | +void VideoOutput::createQmlContentItem() |
1941 | +{ |
1942 | + m_qmlItem = qobject_cast<QQuickItem*>(m_qmlContentComponent->create()); |
1943 | + m_qmlItem->setParentItem(this); |
1944 | + |
1945 | + updateProperties(); |
1946 | +} |
1947 | + |
1948 | +void VideoOutput:: updateProperties() |
1949 | +{ |
1950 | + if (!m_qmlItem) return; |
1951 | + |
1952 | + MediaPlayer* mediaPlayer = qobject_cast<MediaPlayer*>(m_source); |
1953 | + if (!mediaPlayer) return; |
1954 | + |
1955 | + QQmlProperty playbackStateProperty(m_qmlItem, "playbackState"); |
1956 | + playbackStateProperty.write(QVariant::fromValue<int>(mediaPlayer->playbackState())); |
1957 | + |
1958 | + QQmlProperty positionProperty(m_qmlItem, "position"); |
1959 | + positionProperty.write(QVariant::fromValue(mediaPlayer->position())); |
1960 | + |
1961 | + QQmlProperty propResSource(mediaPlayer->metaData(), "resolution"); |
1962 | + QQmlProperty propResDest(m_qmlItem, "resolution"); |
1963 | + |
1964 | + if (propResSource.isValid() && propResDest.isValid()) { |
1965 | + propResDest.write(propResSource.read()); |
1966 | + } |
1967 | +} |
1968 | |
1969 | === added file 'tests/mocks/QtMultimedia/videooutput.h' |
1970 | --- tests/mocks/QtMultimedia/videooutput.h 1970-01-01 00:00:00 +0000 |
1971 | +++ tests/mocks/QtMultimedia/videooutput.h 2015-07-10 14:23:36 +0000 |
1972 | @@ -0,0 +1,52 @@ |
1973 | +/* |
1974 | + * Copyright (C) 2015 Canonical, Ltd. |
1975 | + * |
1976 | + * This program is free software; you can redistribute it and/or modify |
1977 | + * it under the terms of the GNU General Public License as published by |
1978 | + * the Free Software Foundation; version 3. |
1979 | + * |
1980 | + * This program is distributed in the hope that it will be useful, |
1981 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
1982 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1983 | + * GNU General Public License for more details. |
1984 | + * |
1985 | + * You should have received a copy of the GNU General Public License |
1986 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
1987 | + */ |
1988 | + |
1989 | +#ifndef VIDEOOUTPUT_H |
1990 | +#define VIDEOOUTPUT_H |
1991 | + |
1992 | +#include <QQuickItem> |
1993 | +#include <QPointer> |
1994 | +class QQmlComponent; |
1995 | + |
1996 | +class VideoOutput : public QQuickItem |
1997 | +{ |
1998 | + Q_OBJECT |
1999 | + Q_PROPERTY(QObject* source READ source WRITE setSource NOTIFY sourceChanged) |
2000 | +public: |
2001 | + explicit VideoOutput(QQuickItem *parent = 0); |
2002 | + |
2003 | + QObject *source() const { return m_source.data(); } |
2004 | + void setSource(QObject *source); |
2005 | + |
2006 | + void itemChange(ItemChange change, const ItemChangeData & value); |
2007 | + |
2008 | +Q_SIGNALS: |
2009 | + void sourceChanged(); |
2010 | + |
2011 | +protected Q_SLOTS: |
2012 | + void onComponentStatusChanged(QQmlComponent::Status status); |
2013 | + void updateProperties(); |
2014 | + |
2015 | +private: |
2016 | + void createQmlContentItem(); |
2017 | + void printComponentErrors(); |
2018 | + |
2019 | + QPointer<QObject> m_source; |
2020 | + QQmlComponent* m_qmlContentComponent; |
2021 | + QQuickItem* m_qmlItem; |
2022 | +}; |
2023 | + |
2024 | +#endif // VIDEOOUTPUT_H |
2025 | |
2026 | === modified file 'tests/qmltests/CMakeLists.txt' |
2027 | --- tests/qmltests/CMakeLists.txt 2015-06-18 19:23:08 +0000 |
2028 | +++ tests/qmltests/CMakeLists.txt 2015-07-10 14:23:36 +0000 |
2029 | @@ -10,6 +10,7 @@ |
2030 | add_unity8_qmltest(Components DraggingArea) |
2031 | add_unity8_qmltest(Components LazyImage) |
2032 | add_unity8_qmltest(Components Lockscreen) |
2033 | +add_unity8_qmltest(Components MediaServices) |
2034 | add_unity8_qmlunittest(Components PhysicalKeysMapper) |
2035 | add_unity8_qmltest(Components Rating) |
2036 | add_unity8_qmltest(Components ResponsiveGridView) |
2037 | @@ -30,6 +31,7 @@ |
2038 | add_unity8_qmltest(Dash/Previews PreviewExpandable) |
2039 | add_unity8_qmltest(Dash/Previews PreviewHeader) |
2040 | add_unity8_qmltest(Dash/Previews PreviewImageGallery) |
2041 | +add_unity8_qmltest(Dash/Previews PreviewInlineVideo) |
2042 | add_unity8_qmltest(Dash/Previews PreviewPayments) |
2043 | add_unity8_qmltest(Dash/Previews PreviewProgress) |
2044 | add_unity8_qmltest(Dash/Previews PreviewRatingDisplay) |
2045 | |
2046 | === added file 'tests/qmltests/Components/tst_MediaServices.qml' |
2047 | --- tests/qmltests/Components/tst_MediaServices.qml 1970-01-01 00:00:00 +0000 |
2048 | +++ tests/qmltests/Components/tst_MediaServices.qml 2015-07-10 14:23:36 +0000 |
2049 | @@ -0,0 +1,228 @@ |
2050 | +/* |
2051 | + * Copyright 2015 Canonical Ltd. |
2052 | + * |
2053 | + * This program is free software; you can redistribute it and/or modify |
2054 | + * it under the terms of the GNU General Public License as published by |
2055 | + * the Free Software Foundation; version 3. |
2056 | + * |
2057 | + * This program is distributed in the hope that it will be useful, |
2058 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
2059 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2060 | + * GNU General Public License for more details. |
2061 | + * |
2062 | + * You should have received a copy of the GNU General Public License |
2063 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
2064 | + */ |
2065 | + |
2066 | +import QtQuick 2.0 |
2067 | +import QtTest 1.0 |
2068 | +import "../../../qml/Components/MediaServices" |
2069 | +import Unity.Test 0.1 as UT |
2070 | +import Ubuntu.Components 1.2 |
2071 | +import QtMultimedia 5.0 |
2072 | + |
2073 | +Rectangle { |
2074 | + id: root |
2075 | + width: units.gu(70) |
2076 | + height: units.gu(80) |
2077 | + color: "lightgrey" |
2078 | + |
2079 | + property var sourceData: { |
2080 | + "source": "file:///home/nick/Videos/test-mpeg.ogv", |
2081 | + "screenshot": Qt.resolvedUrl("../Dash/artwork/avatar.png") |
2082 | + } |
2083 | + |
2084 | + property var sourceData2: { |
2085 | + "source": "file:///home/nick/Videos/test-mpeg2.ogv", |
2086 | + "screenshot": Qt.resolvedUrl("../Dash/artwork/checkers.png") |
2087 | + } |
2088 | + |
2089 | + MediaDataSource { |
2090 | + source: root.sourceData["source"] |
2091 | + duration: 60000 |
2092 | + metaData: { |
2093 | + "title" : "TEST MPEG", |
2094 | + "resolution" : { "width": 160, "height": 90 } |
2095 | + } |
2096 | + } |
2097 | + |
2098 | + MediaDataSource { |
2099 | + source: root.sourceData2["source"] |
2100 | + duration: 30000 |
2101 | + metaData: { |
2102 | + "title" : "TEST MPEG", |
2103 | + "resolution" : { "width": 90, "height": 240 } |
2104 | + } |
2105 | + } |
2106 | + |
2107 | + Item { |
2108 | + anchors.fill: parent |
2109 | + |
2110 | + Rectangle { |
2111 | + anchors { |
2112 | + top: parent.top |
2113 | + bottom: parent.bottom |
2114 | + left: parent.left |
2115 | + right: controls.left |
2116 | + } |
2117 | + color: "darkblue" |
2118 | + |
2119 | + MediaServices { |
2120 | + id: services |
2121 | + fullscreen: false |
2122 | + width: parent.width |
2123 | + sourceData: root.sourceData |
2124 | + context: "video" |
2125 | + |
2126 | + rootItem: parent |
2127 | + maximumEmbeddedHeight: undefined |
2128 | + } |
2129 | + } |
2130 | + |
2131 | + Rectangle { |
2132 | + id: controls |
2133 | + color: "darkgrey" |
2134 | + anchors { |
2135 | + top: parent.top |
2136 | + bottom: parent.bottom |
2137 | + right: parent.right |
2138 | + } |
2139 | + width: units.gu(30) |
2140 | + |
2141 | + Column { |
2142 | + anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) } |
2143 | + |
2144 | + Button { |
2145 | + text: "Fullscreen" |
2146 | + onClicked: services.fullscreen = !services.fullscreen |
2147 | + } |
2148 | + UT.MouseTouchEmulationCheckbox {} |
2149 | + } |
2150 | + } |
2151 | + } |
2152 | + |
2153 | + SignalSpy { |
2154 | + id: serviceCloseSpy |
2155 | + target: services |
2156 | + signalName: "close" |
2157 | + } |
2158 | + |
2159 | + UT.UnityTestCase { |
2160 | + name: "VideoMediaServices" |
2161 | + when: windowShown |
2162 | + |
2163 | + property int oldControlTimerInterval: 0; |
2164 | + |
2165 | + function init() { |
2166 | + services.sourceData = root.sourceData |
2167 | + services.context = "video"; |
2168 | + |
2169 | + var controlHideTimer = findInvisibleChild(services, "controlHideTimer"); |
2170 | + verify(controlHideTimer !== null); |
2171 | + oldControlTimerInterval = controlHideTimer.interval; |
2172 | + } |
2173 | + |
2174 | + function cleanup() { |
2175 | + serviceCloseSpy.clear(); |
2176 | + services.context = ""; |
2177 | + services.fullscreen = false; |
2178 | + services.maximumEmbeddedHeight = undefined; |
2179 | + tryCompareFunction(function() { return services.header ? services.header.visible : false }, false); |
2180 | + tryCompareFunction(function() { return services.footer ? services.footer.visible : false }, false); |
2181 | + |
2182 | + var controlHideTimer = findInvisibleChild(services, "controlHideTimer"); |
2183 | + controlHideTimer.interval = oldControlTimerInterval; |
2184 | + } |
2185 | + |
2186 | + function test_videoWidget() { |
2187 | + var videoPlayer = findChild(services, "videoPlayer"); |
2188 | + verify(videoPlayer !== null); |
2189 | + |
2190 | + var videoControls = findChild(services, "videoControls"); |
2191 | + verify(videoControls !== null); |
2192 | + } |
2193 | + |
2194 | + function test_tapVideoPlayerToPlayAndPause() { |
2195 | + var videoPlayer = findChild(services, "videoPlayer"); |
2196 | + verify(videoPlayer !== null) |
2197 | + tap(videoPlayer, videoPlayer.width/2, videoPlayer.height/2); |
2198 | + |
2199 | + var mediaPlayer = findInvisibleChild(services, "mediaPlayer"); |
2200 | + verify(mediaPlayer !== null); |
2201 | + compare(mediaPlayer.playbackState, MediaPlayer.PlayingState, "Media player should be playing"); |
2202 | + |
2203 | + tap(videoPlayer, videoPlayer.width/2, videoPlayer.height/2); |
2204 | + compare(mediaPlayer.playbackState, MediaPlayer.PausedState, "Media player should be playing"); |
2205 | + } |
2206 | + |
2207 | + function test_fullscreenSwitch() { |
2208 | + var mediaPlayer = findInvisibleChild(services, "mediaPlayer"); |
2209 | + verify(mediaPlayer !== null); |
2210 | + mediaPlayer.play(); |
2211 | + wait(UbuntuAnimation.BriskDuration); // animation |
2212 | + |
2213 | + var button = findChild(services, "viewActionButton"); |
2214 | + verify(button !== null); |
2215 | + tap(button); |
2216 | + |
2217 | + compare(services.fullscreen, true, "Should have switched to fullscreen mode."); |
2218 | + } |
2219 | + |
2220 | + function test_HeaderVisibleOnlyWhenFullscreen() { |
2221 | + services.fullscreen = false; |
2222 | + compare(services.header, null, "Header should be null when not fullscreen"); |
2223 | + services.fullscreen = true; |
2224 | + tryCompareFunction(function() { return services.header !== null }, true); |
2225 | + } |
2226 | + |
2227 | + function test_ControlsShowAndHideWhenPlayed() { |
2228 | + services.fullscreen = true; |
2229 | + var controlHideTimer = findInvisibleChild(services, "controlHideTimer"); |
2230 | + controlHideTimer.interval = 100; |
2231 | + |
2232 | + var mediaPlayer = findInvisibleChild(services, "mediaPlayer"); |
2233 | + mediaPlayer.play(); |
2234 | + |
2235 | + tryCompare(services.header, "visible", true); |
2236 | + tryCompare(services.footer, "visible", true); |
2237 | + |
2238 | + tryCompare(services.header, "visible", false); |
2239 | + tryCompare(services.footer, "visible", false); |
2240 | + } |
2241 | + |
2242 | + function test_ControlsDontTimeOutWhenPaused() { |
2243 | + services.fullscreen = true; |
2244 | + var controlHideTimer = findInvisibleChild(services, "controlHideTimer"); |
2245 | + controlHideTimer.interval = 100; |
2246 | + |
2247 | + var mediaPlayer = findInvisibleChild(services, "mediaPlayer"); |
2248 | + mediaPlayer.play(); |
2249 | + mediaPlayer.pause(); |
2250 | + wait(300); |
2251 | + |
2252 | + compare(services.header.visible, true, "Header should still be visible"); |
2253 | + compare(services.footer.visible, true, "Footer should still be visible"); |
2254 | + } |
2255 | + |
2256 | + function test_close() { |
2257 | + services.fullscreen = true; |
2258 | + var mediaPlayer = findInvisibleChild(services, "mediaPlayer"); |
2259 | + mediaPlayer.play(); |
2260 | + mediaPlayer.pause(); |
2261 | + wait(UbuntuAnimation.BriskDuration); // animation |
2262 | + |
2263 | + var navigationButton = findChild(services, "navigationButton"); |
2264 | + verify(navigationButton !== null); |
2265 | + |
2266 | + tap(navigationButton); |
2267 | + compare(serviceCloseSpy.count, 1, "close was not called"); |
2268 | + } |
2269 | + |
2270 | + function test_maximumVideoSize() { |
2271 | + services.maximumEmbeddedHeight = root.height / 2 |
2272 | + services.sourceData = root.sourceData2 |
2273 | + verify(services.content !== null); |
2274 | + tryCompare(services.content, "height", root.height/2); |
2275 | + } |
2276 | + } |
2277 | +} |
2278 | |
2279 | === added file 'tests/qmltests/Dash/Previews/tst_PreviewInlineVideo.qml' |
2280 | --- tests/qmltests/Dash/Previews/tst_PreviewInlineVideo.qml 1970-01-01 00:00:00 +0000 |
2281 | +++ tests/qmltests/Dash/Previews/tst_PreviewInlineVideo.qml 2015-07-10 14:23:36 +0000 |
2282 | @@ -0,0 +1,106 @@ |
2283 | +/* |
2284 | + * Copyright 2015 Canonical Ltd. |
2285 | + * |
2286 | + * This program is free software; you can redistribute it and/or modify |
2287 | + * it under the terms of the GNU General Public License as published by |
2288 | + * the Free Software Foundation; version 3. |
2289 | + * |
2290 | + * This program is distributed in the hope that it will be useful, |
2291 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
2292 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2293 | + * GNU General Public License for more details. |
2294 | + * |
2295 | + * You should have received a copy of the GNU General Public License |
2296 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
2297 | + */ |
2298 | + |
2299 | +import QtQuick 2.0 |
2300 | +import QtTest 1.0 |
2301 | +import "../../../../qml/Dash/Previews" |
2302 | +import Unity.Test 0.1 as UT |
2303 | +import Ubuntu.Components 1.2 |
2304 | +import QtMultimedia 5.0 |
2305 | + |
2306 | +Rectangle { |
2307 | + width: units.gu(70) |
2308 | + height: units.gu(80) |
2309 | + color: "lightgrey" |
2310 | + |
2311 | + property var widgetData0: { |
2312 | + "source": "file:///test-video1", |
2313 | + "screenshot": Qt.resolvedUrl("../artwork/avatar.png") |
2314 | + } |
2315 | + |
2316 | + property var widgetData1: { |
2317 | + "source": "file:///test-video2", |
2318 | + "screenshot": "file:///test-video2-screenshot" |
2319 | + } |
2320 | + |
2321 | + MediaDataSource { |
2322 | + source: "file:///test-video1" |
2323 | + duration: 6000000 |
2324 | + metaData: { |
2325 | + "title" : "TEST MPEG", |
2326 | + "resolution" : { "width": 1920, "height": 1080 } |
2327 | + } |
2328 | + } |
2329 | + |
2330 | + Item { |
2331 | + anchors.fill: parent |
2332 | + |
2333 | + Item { |
2334 | + id: inner |
2335 | + anchors { |
2336 | + top: parent.top |
2337 | + bottom: parent.bottom |
2338 | + left: parent.left |
2339 | + right: controls.left |
2340 | + } |
2341 | + |
2342 | + Item { |
2343 | + anchors.fill: parent |
2344 | + anchors.margins: units.gu(2) |
2345 | + |
2346 | + PreviewInlineVideo { |
2347 | + id: videoPlayback |
2348 | + width: parent.width |
2349 | + widgetData: widgetData0 |
2350 | + |
2351 | + rootItem: inner |
2352 | + } |
2353 | + } |
2354 | + |
2355 | + } |
2356 | + |
2357 | + Rectangle { |
2358 | + id: controls |
2359 | + color: "darkgrey" |
2360 | + anchors { |
2361 | + top: parent.top |
2362 | + bottom: parent.bottom |
2363 | + right: parent.right |
2364 | + } |
2365 | + width: units.gu(30) |
2366 | + |
2367 | + Column { |
2368 | + anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) } |
2369 | + |
2370 | + UT.MouseTouchEmulationCheckbox {} |
2371 | + } |
2372 | + } |
2373 | + } |
2374 | + |
2375 | + UT.UnityTestCase { |
2376 | + name: "PreviewInlineVideoTest" |
2377 | + when: windowShown |
2378 | + |
2379 | + function test_loadScreenshot() { |
2380 | + var screenshot = findChild(videoPlayback, "screenshot"); |
2381 | + verify(screenshot !== null); |
2382 | + |
2383 | + videoPlayback.widgetData = widgetData1; |
2384 | + var screenshotSource = screenshot.source |
2385 | + compare(screenshotSource.toString(), "file:///test-video2-screenshot"); |
2386 | + } |
2387 | + } |
2388 | +} |
2389 | |
2390 | === modified file 'tests/qmltests/Dash/Previews/tst_PreviewWidgetFactory.qml' |
2391 | --- tests/qmltests/Dash/Previews/tst_PreviewWidgetFactory.qml 2014-08-26 19:21:31 +0000 |
2392 | +++ tests/qmltests/Dash/Previews/tst_PreviewWidgetFactory.qml 2015-07-10 14:23:36 +0000 |
2393 | @@ -75,24 +75,25 @@ |
2394 | |
2395 | function test_mapping_data() { |
2396 | return [ |
2397 | - { tag: "Actions", type: "actions", source: "PreviewActions.qml" }, |
2398 | - { tag: "Audio", type: "audio", source: "PreviewAudioPlayback.qml" }, |
2399 | - { tag: "Expandable", type: "expandable", source: "PreviewExpandable.qml" }, |
2400 | - { tag: "Gallery", type: "gallery", source: "PreviewImageGallery.qml" }, |
2401 | - { tag: "Header", type: "header", source: "PreviewHeader.qml" }, |
2402 | - { tag: "Image", type: "image", source: "PreviewZoomableImage.qml" }, |
2403 | - { tag: "Progress", type: "progress", source: "PreviewProgress.qml" }, |
2404 | - { tag: "Rating Input", type: "rating-input", source: "PreviewRatingInput.qml" }, |
2405 | - { tag: "Rating Display", type: "reviews", source: "PreviewRatingDisplay.qml" }, |
2406 | - { tag: "Table", type: "table", source: "PreviewTable.qml" }, |
2407 | - { tag: "Text", type: "text", source: "PreviewTextSummary.qml" }, |
2408 | - { tag: "Video", type: "video", source: "PreviewVideoPlayback.qml" }, |
2409 | + { tag: "Actions", data: { type: "actions" }, source: "PreviewActions.qml" }, |
2410 | + { tag: "Audio", data: { type: "audio" }, source: "PreviewAudioPlayback.qml" }, |
2411 | + { tag: "Expandable", data: { type: "expandable" }, source: "PreviewExpandable.qml" }, |
2412 | + { tag: "Gallery", data: { type: "gallery" }, source: "PreviewImageGallery.qml" }, |
2413 | + { tag: "Header", data: { type: "header" }, source: "PreviewHeader.qml" }, |
2414 | + { tag: "Image", data: { type: "image" }, source: "PreviewZoomableImage.qml" }, |
2415 | + { tag: "Progress", data: { type: "progress" }, source: "PreviewProgress.qml" }, |
2416 | + { tag: "Rating Input", data: { type: "rating-input" }, source: "PreviewRatingInput.qml" }, |
2417 | + { tag: "Rating Display", data: { type: "reviews" }, source: "PreviewRatingDisplay.qml" }, |
2418 | + { tag: "Table", data: { type: "table" }, source: "PreviewTable.qml" }, |
2419 | + { tag: "Text", data: { type: "text" }, source: "PreviewTextSummary.qml" }, |
2420 | + { tag: "Video", data: { type: "video" }, source: "PreviewInlineVideo.qml" }, |
2421 | + { tag: "Video", data: { type: "video", source: "http://demo" }, source: "PreviewVideoPlayback.qml" }, |
2422 | ]; |
2423 | } |
2424 | |
2425 | function test_mapping(data) { |
2426 | - factory.widgetData = { type: data.type }; |
2427 | - factory.widgetType = data.type; |
2428 | + factory.widgetData = data.data; |
2429 | + factory.widgetType = data.data.type; |
2430 | |
2431 | verify((String(factory.source)).indexOf(data.source) != -1); |
2432 | } |
Commented out sections in VideoPlayer is waiting on a design review (may need the ShaderEffect), and the in PreviewInlineVi deo.qml is for sharing via content hub.
Will be removed before merging.