Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qwasmvideooutputevents.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4// Event wiring for QWasmVideoOutput: the HTML media element callbacks
5// and the iOS orientation-change handling. Split into its own translation
6// unit to keep qwasmvideooutput.cpp focused on lifecycle and playback control.
7
9
10#include <QDebug>
11#include <QRect>
12
13#include <emscripten/val.h>
14#include <emscripten/html5.h>
15
17
19{
20 qCDebug(qWasmMediaVideoOutput) << Q_FUNC_INFO;
21
22 // event callbacks
23 // timupdate
24 auto timeUpdateCallback = [=](emscripten::val event) {
25 qCDebug(qWasmMediaVideoOutput) << "timeupdate";
26
27 // qt progress is ms
28 emit progressChanged(event["target"]["currentTime"].as<double>() * 1000);
29 };
30 m_timeUpdateEvent.reset(new QWasmEventHandler(m_video, "timeupdate", timeUpdateCallback));
31
32 // play
33 auto playCallback = [=](emscripten::val event) {
34 Q_UNUSED(event)
35 qCDebug(qWasmMediaVideoOutput) << "play" << m_video["src"].as<std::string>();
36 if (!m_isSeeking)
37 emit stateChanged(QWasmMediaPlayer::Preparing);
38 };
39 m_playEvent.reset(new QWasmEventHandler(m_video, "play", playCallback));
40
41 // ended
42 auto endedCallback = [=](emscripten::val event) {
43 Q_UNUSED(event)
44 qCDebug(qWasmMediaVideoOutput) << "ended";
45 m_currentMediaStatus = MediaStatus::EndOfMedia;
46 emit statusChanged(m_currentMediaStatus);
47 };
48 m_endedEvent.reset(new QWasmEventHandler(m_video, "ended", endedCallback));
49
50 // durationchange
51 auto durationChangeCallback = [=](emscripten::val event) {
52 qCDebug(qWasmMediaVideoOutput) << "durationChange";
53
54 // qt duration is in milliseconds.
55 qint64 dur = event["target"]["duration"].as<double>() * 1000;
56 emit durationChanged(dur);
57 };
58 m_durationChangeEvent.reset(
59 new QWasmEventHandler(m_video, "durationchange", durationChangeCallback));
60
61 // loadeddata
62 auto loadedDataCallback = [=](emscripten::val event) {
63 Q_UNUSED(event)
64 qCDebug(qWasmMediaVideoOutput) << "loaded data";
65
66 emit stateChanged(QWasmMediaPlayer::Prepared);
67 if (m_isSeekable != isVideoSeekable()) {
68 m_isSeekable = isVideoSeekable();
69 emit seekableChanged(m_isSeekable);
70 }
71 };
72 m_loadedDataEvent.reset(new QWasmEventHandler(m_video, "loadeddata", loadedDataCallback));
73
74 // error
75 auto errorCallback = [=](emscripten::val event) {
76 qCDebug(qWasmMediaVideoOutput) << "error";
77 if (event.isUndefined() || event.isNull())
78 return;
79 emit errorOccured(m_video["error"]["code"].as<int>(),
80 QString::fromStdString(m_video["error"]["message"].as<std::string>()));
81 };
82 m_errorChangeEvent.reset(new QWasmEventHandler(m_video, "error", errorCallback));
83
84 // resize
85 auto resizeCallback = [=](emscripten::val event) {
86 Q_UNUSED(event)
87 qCDebug(qWasmMediaVideoOutput) << "resize";
88
89 updateVideoElementGeometry(
90 QRect(0, 0, m_video["videoWidth"].as<int>(), m_video["videoHeight"].as<int>()));
91 emit sizeChange(m_video["videoWidth"].as<int>(), m_video["videoHeight"].as<int>());
92
93 };
94 m_resizeChangeEvent.reset(new QWasmEventHandler(m_video, "resize", resizeCallback));
95
96 // loadedmetadata
97 auto loadedMetadataCallback = [=](emscripten::val event) {
98 Q_UNUSED(event)
99 qCDebug(qWasmMediaVideoOutput) << "loaded meta data";
100
101 emit metaDataLoaded();
102 };
103 m_loadedMetadataChangeEvent.reset(
104 new QWasmEventHandler(m_video, "loadedmetadata", loadedMetadataCallback));
105
106 // loadstart
107 auto loadStartCallback = [=](emscripten::val event) {
108 Q_UNUSED(event)
109 qCDebug(qWasmMediaVideoOutput) << "load started";
110 m_currentMediaStatus = MediaStatus::LoadingMedia;
111 emit statusChanged(m_currentMediaStatus);
112 m_isStopped = false;
113 };
114 m_loadStartChangeEvent.reset(new QWasmEventHandler(m_video, "loadstart", loadStartCallback));
115
116 // canplay
117
118 auto canPlayCallback = [=](emscripten::val event) {
119 if (event.isUndefined() || event.isNull())
120 return;
121 qCDebug(qWasmMediaVideoOutput) << "can play"
122 << "m_requestedPosition" << m_requestedPosition;
123
124 if (!m_isStopped)
125 emit readyChanged(true); // sets video available
126 };
127 m_canPlayChangeEvent.reset(new QWasmEventHandler(m_video, "canplay", canPlayCallback));
128
129 // canplaythrough
130 auto canPlayThroughCallback = [=](emscripten::val event) {
131 Q_UNUSED(event)
132 qCDebug(qWasmMediaVideoOutput) << "can play through"
133 << "m_isStopped" << m_isStopped;
134
135 if (m_currentMediaStatus == MediaStatus::EndOfMedia)
136 return;
137 bool seekable = isVideoSeekable();
138 if (m_isSeekable != seekable) {
139 m_isSeekable = seekable;
140 emit seekableChanged(m_isSeekable);
141 }
142 if (!m_isSeeking && !m_isStopped) {
143 emscripten::val timeRanges = m_video["buffered"];
144 if ((!timeRanges.isNull() || !timeRanges.isUndefined())
145 && timeRanges["length"].as<int>() == 1) {
146 double buffered = m_video["buffered"].call<emscripten::val>("end", 0).as<double>();
147 const double duration = m_video["duration"].as<double>();
148
149 if (duration == buffered) {
150 m_currentBufferedValue = 100;
151 emit bufferingChanged(m_currentBufferedValue);
152 }
153 }
154 constexpr int hasEnoughData = 4;
155 if (m_video["readyState"].as<int>() == hasEnoughData) {
156 m_currentMediaStatus = MediaStatus::LoadedMedia;
157 emit statusChanged(m_currentMediaStatus);
158 m_frameGrabber.startFrameLoop();
159 }
160 } else {
161 m_isStopped = false;
162 }
163 };
164 m_canPlayThroughChangeEvent.reset(
165 new QWasmEventHandler(m_video, "canplaythrough", canPlayThroughCallback));
166
167 // seeking
168 auto seekingCallback = [=](emscripten::val event) {
169 Q_UNUSED(event)
170 qCDebug(qWasmMediaVideoOutput)
171 << "seeking started" << (m_video["currentTime"].as<double>() * 1000);
172 m_isSeeking = true;
173 };
174 m_seekingChangeEvent.reset(new QWasmEventHandler(m_video, "seeking", seekingCallback));
175
176 // seeked
177 auto seekedCallback = [=](emscripten::val event) {
178 Q_UNUSED(event)
179 qCDebug(qWasmMediaVideoOutput) << "seeked" << (m_video["currentTime"].as<double>() * 1000);
180 emit progressChanged(m_video["currentTime"].as<double>() * 1000);
181 m_isSeeking = false;
182 };
183 m_seekedChangeEvent.reset(new QWasmEventHandler(m_video, "seeked", seekedCallback));
184
185 // emptied
186 auto emptiedCallback = [=](emscripten::val event) {
187 Q_UNUSED(event)
188 qCDebug(qWasmMediaVideoOutput) << "emptied";
189 emit readyChanged(false);
190 m_currentMediaStatus = MediaStatus::EndOfMedia;
191 emit statusChanged(m_currentMediaStatus);
192 };
193 m_emptiedChangeEvent.reset(new QWasmEventHandler(m_video, "emptied", emptiedCallback));
194
195 // stalled
196 auto stalledCallback = [=](emscripten::val event) {
197 Q_UNUSED(event)
198 qCDebug(qWasmMediaVideoOutput) << "stalled";
199 m_currentMediaStatus = MediaStatus::StalledMedia;
200 emit statusChanged(m_currentMediaStatus);
201 };
202 m_stalledChangeEvent.reset(new QWasmEventHandler(m_video, "stalled", stalledCallback));
203
204 // waiting
205 auto waitingCallback = [=](emscripten::val event) {
206 Q_UNUSED(event)
207
208 qCDebug(qWasmMediaVideoOutput) << "waiting";
209 // check buffer
210 };
211 m_waitingChangeEvent.reset(new QWasmEventHandler(m_video, "waiting", waitingCallback));
212
213 // suspend
214
215 // playing
216 auto playingCallback = [=](emscripten::val event) {
217 Q_UNUSED(event)
218 qCDebug(qWasmMediaVideoOutput) << "playing";
219 if (m_isSeeking)
220 return;
221 emit stateChanged(QWasmMediaPlayer::Started);
222 if (m_toBePaused) { // paused
223 m_toBePaused = false;
224 m_frameGrabber.startFrameLoop();
225 }
226 };
227 m_playingChangeEvent.reset(new QWasmEventHandler(m_video, "playing", playingCallback));
228
229 // progress (buffering progress)
230 auto progesssCallback = [=](emscripten::val event) {
231 if (event.isUndefined() || event.isNull())
232 return;
233
234 const double duration = event["target"]["duration"].as<double>();
235 if (duration < 0) // track not exactly ready yet
236 return;
237
238 emscripten::val timeRanges = event["target"]["buffered"];
239
240 if ((!timeRanges.isNull() || !timeRanges.isUndefined())
241 && timeRanges["length"].as<int>() == 1) {
242 emscripten::val dVal = timeRanges.call<emscripten::val>("end", 0);
243 if (!dVal.isNull() || !dVal.isUndefined()) {
244 double bufferedEnd = dVal.as<double>();
245
246 if (duration > 0 && bufferedEnd > 0) {
247 const double bufferedValue = (bufferedEnd / duration * 100);
248 qCDebug(qWasmMediaVideoOutput) << "progress buffered";
249 m_currentBufferedValue = bufferedValue;
250 emit bufferingChanged(m_currentBufferedValue);
251 if (bufferedEnd == duration)
252 m_currentMediaStatus = MediaStatus::BufferedMedia;
253 else
254 m_currentMediaStatus = MediaStatus::BufferingMedia;
255 emit statusChanged(m_currentMediaStatus);
256 }
257 }
258 }
259 };
260 m_progressChangeEvent.reset(new QWasmEventHandler(m_video, "progress", progesssCallback));
261
262 // pause
263 auto pauseCallback = [=](emscripten::val event) {
264 Q_UNUSED(event)
265 qCDebug(qWasmMediaVideoOutput) << "pause";
266 m_toBePaused = true;
267 const double currentTime = m_video["currentTime"].as<double>(); // in seconds
268 const double duration = m_video["duration"].as<double>(); // in seconds
269 if ((currentTime > 0 && currentTime < duration) && (!m_isStopped)) {
270 emit stateChanged(QWasmMediaPlayer::Paused);
271 } else {
272 // stop this crazy thing!
273 m_video.set("currentTime", emscripten::val(0));
274 emit stateChanged(QWasmMediaPlayer::Stopped);
275 }
276 };
277 m_pauseChangeEvent.reset(new QWasmEventHandler(m_video, "pause", pauseCallback));
278
279 // onunload
280 // we use lower level events here as to avert a crash on activate using the
281 // qtdweb see _qt_beforeUnload
282 emscripten::val window = emscripten::val::global("window");
283
284 auto beforeUnloadCallback = [=](emscripten::val event) {
285 Q_UNUSED(event)
286 // large videos will leave the unloading window
287 // in a frozen state, so remove the video element src first
288 m_video.call<void>("removeAttribute", emscripten::val("src"));
289 m_video.call<void>("load");
290 };
291 m_beforeUnloadEvent.reset(new QWasmEventHandler(window, "beforeunload", beforeUnloadCallback));
292}
293
294// iOS has [broken] camera driver, so rotate the frames ourselves
295// based on the device orientation and which camera is facing.
296void QWasmVideoOutput::applyIosRotation(int orientationIndex)
297{
298 if (orientationIndex & EMSCRIPTEN_ORIENTATION_PORTRAIT_PRIMARY) { // 1
299 m_rotateBy = QtVideo::Rotation::Clockwise90;
300 } else if (orientationIndex & EMSCRIPTEN_ORIENTATION_LANDSCAPE_PRIMARY) { // 4
301 if (m_cameraMode == QWasmVideoOutput::Front) {
302 m_rotateBy = QtVideo::Rotation::Clockwise180;
303 } else {
304 m_rotateBy = QtVideo::Rotation::None;
305 }
306 } else if (orientationIndex & EMSCRIPTEN_ORIENTATION_PORTRAIT_SECONDARY) { // 2
307 m_rotateBy = QtVideo::Rotation::Clockwise270;
308 } else if (orientationIndex & EMSCRIPTEN_ORIENTATION_LANDSCAPE_SECONDARY) { // 8
309 if (m_cameraMode == QWasmVideoOutput::Front) {
310 m_rotateBy = QtVideo::Rotation::None;
311 } else {
312 m_rotateBy = QtVideo::Rotation::Clockwise180;
313 }
314 }
315}
316
317bool QWasmVideoOutput::orientationchangeCallback(int eventType,
318 const EmscriptenOrientationChangeEvent *event,
319 void *userData)
320{
321 Q_UNUSED(eventType)
322
323 QWasmVideoOutput *videoOutput = static_cast<QWasmVideoOutput *>(userData);
324 emit videoOutput->orientationChanged(event->orientationIndex);
325
326 return true;
327}
328
329int QWasmVideoOutput::getCurrentOrientationIndex()
330{
331 //get current status
332 EmscriptenOrientationChangeEvent status;
333 EMSCRIPTEN_RESULT result = emscripten_get_orientation_status(&status);
334 if (result == EMSCRIPTEN_RESULT_SUCCESS)
335 return status.orientationIndex;
336 return 0;
337}
338
339void QWasmVideoOutput::detectIosCameraRotation()
340{
341 if (!isPlatformiOs())
342 return;
343
344 m_useCameraRotation = true;
345 emscripten::val stream = m_video["srcObject"];
346 emscripten::val videoTracks = stream.call<emscripten::val>("getVideoTracks");
347
348 if (!videoTracks.isUndefined() && videoTracks["length"].as<int>() > 0) {
349 emscripten::val track = videoTracks[0];
350 emscripten::val settings = track.call<emscripten::val>("getSettings");
351
352 if (settings["facingMode"].as<std::string>() == "user")
353 m_cameraMode = QWasmVideoOutput::Front;
354 else
355 m_cameraMode = QWasmVideoOutput::Back;
356 // now we know camera, set m_rotateBy
357 orientationChanged(getCurrentOrientationIndex());
358 }
359}
360
361QT_END_NAMESPACE
void orientationChanged(int rotationIndex)
Combined button and popup list for selecting options.