1 // This may look like C code, but it's really -*- C++ -*-
2 /*
3  * Copyright (C) 2010 Emweb bv, Herent, Belgium.
4  *
5  * See the LICENSE file for terms of use.
6  */
7 #include "Wt/WAbstractMedia.h"
8 #include "Wt/WApplication.h"
9 #include "Wt/WEnvironment.h"
10 #include "Wt/WException.h"
11 #include "Wt/WResource.h"
12 #include "DomElement.h"
13 
14 #include "StringUtils.h"
15 #include "WebUtils.h"
16 
17 #ifndef WT_DEBUG_JS
18 #include "js/WAbstractMedia.min.js"
19 #endif
20 
21 namespace {
intToReadyState(int i)22   Wt::MediaReadyState intToReadyState(int i)
23   {
24     switch (i) {
25     case 0:
26       return Wt::MediaReadyState::HaveNothing;
27     case 1:
28       return Wt::MediaReadyState::HaveMetaData;
29     case 2:
30       return Wt::MediaReadyState::HaveCurrentData;
31     case 3:
32       return Wt::MediaReadyState::HaveFutureData;
33     case 4:
34       return Wt::MediaReadyState::HaveEnoughData;
35     default:
36       throw Wt::WException("Invalid readystate");
37     }
38   }
39 }
40 
41 using namespace Wt;
42 const char *WAbstractMedia::PLAYBACKSTARTED_SIGNAL = "play";
43 const char *WAbstractMedia::PLAYBACKPAUSED_SIGNAL = "pause";
44 const char *WAbstractMedia::ENDED_SIGNAL = "ended";
45 const char *WAbstractMedia::TIMEUPDATED_SIGNAL = "timeupdate";
46 const char *WAbstractMedia::VOLUMECHANGED_SIGNAL = "volumechange";
47 
WAbstractMedia()48 WAbstractMedia::WAbstractMedia()
49   : sourcesRendered_(0),
50     flags_(None),
51     preloadMode_(MediaPreloadMode::Auto),
52     flagsChanged_(false),
53     preloadChanged_(false),
54     sourcesChanged_(false),
55     playing_(false),
56     volume_(-1),
57     current_(-1),
58     duration_(-1),
59     ended_(false),
60     readyState_(MediaReadyState::HaveNothing)
61 {
62   setInline(false);
63   setFormObject(true);
64 
65 #ifndef WT_TARGET_JAVA
66   implementStateless(&WAbstractMedia::play, &WAbstractMedia::play);
67   implementStateless(&WAbstractMedia::pause, &WAbstractMedia::pause);
68 #endif //WT_TARGET_JAVA
69 }
70 
~WAbstractMedia()71 WAbstractMedia::~WAbstractMedia()
72 {
73   manageWidget(alternative_, std::unique_ptr<WWidget>());
74 }
75 
playbackStarted()76 EventSignal<>& WAbstractMedia::playbackStarted()
77 {
78   return *voidEventSignal(PLAYBACKSTARTED_SIGNAL, true);
79 }
80 
playbackPaused()81 EventSignal<>& WAbstractMedia::playbackPaused()
82 {
83   return *voidEventSignal(PLAYBACKPAUSED_SIGNAL, true);
84 }
85 
ended()86 EventSignal<>& WAbstractMedia::ended()
87 {
88   return *voidEventSignal(ENDED_SIGNAL, true);
89 }
90 
timeUpdated()91 EventSignal<>& WAbstractMedia::timeUpdated()
92 {
93   return *voidEventSignal(TIMEUPDATED_SIGNAL, true);
94 }
95 
volumeChanged()96 EventSignal<>& WAbstractMedia::volumeChanged()
97 {
98   return *voidEventSignal(VOLUMECHANGED_SIGNAL, true);
99 }
100 
setFormData(const FormData & formData)101 void WAbstractMedia::setFormData(const FormData& formData)
102 {
103   if (!Utils::isEmpty(formData.values)) {
104     std::vector<std::string> attributes;
105     boost::split(attributes, formData.values[0], boost::is_any_of(";"));
106     if (attributes.size() == 6) {
107       try {
108 	volume_ = Utils::stod(attributes[0]);
109       } catch (const std::exception& e) {
110         volume_ = -1;
111       }
112       try {
113 	current_ = Utils::stod(attributes[1]);
114       } catch (const std::exception& e) {
115         current_ = -1;
116       }
117       try {
118         duration_ = Utils::stod(attributes[2]);
119       } catch (const std::exception& e) {
120         duration_ = -1;
121       }
122       playing_ = (attributes[3] == "0");
123       ended_ = (attributes[4] == "1");
124       try {
125         readyState_ = intToReadyState(Utils::stoi(attributes[5]));
126       } catch (const std::exception& e) {
127         readyState_ = MediaReadyState::HaveNothing;
128       }
129     } else
130       throw WException("WAbstractMedia: error parsing: "
131 		       + formData.values[0]);
132   }
133 }
134 
play()135 void WAbstractMedia::play()
136 {
137   loadJavaScript();
138   doJavaScript(jsRef() + ".wtObj.play();");
139 }
140 
pause()141 void WAbstractMedia::pause()
142 {
143   loadJavaScript();
144   doJavaScript(jsRef() + ".wtObj.pause();");
145 }
146 
renderSource(DomElement * element,WAbstractMedia::Source & source,bool isLast)147 void WAbstractMedia::renderSource(DomElement* element,
148 				  WAbstractMedia::Source &source, bool isLast)
149 {
150   // src is mandatory
151   element->setAttribute("src", resolveRelativeUrl(source.link.url()));
152 
153   if (source.type != "")
154     element->setAttribute("type", source.type);
155 
156   if (source.media != "")
157     element->setAttribute("media", source.media);
158 
159   if (isLast && alternative_) {
160     // Last element -> add error handler for unsupported content
161     element->setAttribute("onerror",
162       """var media = this.parentNode;"
163       """if(media){"
164       ""  "while (media && media.children.length)"
165       ""    "if (" WT_CLASS ".hasTag(media.firstChild,'SOURCE')){"
166       ""      "media.removeChild(media.firstChild);"
167       ""    "}else{"
168       ""      "media.parentNode.insertBefore(media.firstChild, media);"
169       ""    "}"
170       ""  "media.style.display= 'none';"
171       """}"
172       );
173   } else {
174     element->setAttribute("onerror", "");
175   }
176 }
177 
updateMediaDom(DomElement & element,bool all)178 void WAbstractMedia::updateMediaDom(DomElement& element, bool all)
179 {
180   // Only if not IE
181   if (all && alternative_) {
182     element.setAttribute("onerror",
183       """if(event.target.error && event.target.error.code=="
184       ""   "event.target.error.MEDIA_ERR_SRC_NOT_SUPPORTED){"
185       ""  "while (this.hasChildNodes())"
186       ""    "if (" WT_CLASS ".hasTag(this.firstChild,'SOURCE')){"
187       ""      "this.removeChild(this.firstChild);"
188       ""    "}else{"
189       ""      "this.parentNode.insertBefore(this.firstChild, this);"
190       ""    "}"
191       ""  "this.style.display= 'none';"
192       """}"
193       );
194   }
195   if (all || flagsChanged_) {
196     if ((!all) || flags_.test(PlayerOption::Controls))
197       element.setAttribute("controls",
198 			   flags_.test(PlayerOption::Controls) ? "controls" : "");
199     if ((!all) || flags_.test(PlayerOption::Autoplay))
200       element.setAttribute("autoplay",
201 			   flags_.test(PlayerOption::Autoplay) ? "autoplay" : "");
202     if ((!all) || flags_.test(PlayerOption::Loop))
203       element.setAttribute("loop",
204 			   flags_.test(PlayerOption::Loop) ? "loop" : "");
205   }
206   if (all || preloadChanged_) {
207     switch (preloadMode_) {
208     case MediaPreloadMode::None:
209       element.setAttribute("preload", "none");
210       break;
211     default:
212     case MediaPreloadMode::Auto:
213       element.setAttribute("preload", "auto");
214       break;
215     case MediaPreloadMode::Metadata:
216       element.setAttribute("preload", "metadata");
217       break;
218     }
219   }
220 
221   updateEventSignals(element, all);
222 
223   if (all)
224     if (alternative_) {
225       element.addChild(alternative_->createSDomElement(wApp));
226     }
227   flagsChanged_ = preloadChanged_ = false;
228 }
229 
loadJavaScript()230 void WAbstractMedia::loadJavaScript()
231 {
232   if (javaScriptMember(" WAbstractMedia").empty()) {
233     WApplication *app = WApplication::instance();
234 
235     LOAD_JAVASCRIPT(app, "js/WAbstractMedia.js", "WAbstractMedia", wtjs1);
236 
237     setJavaScriptMember(" WAbstractMedia",
238 			"new " WT_CLASS ".WAbstractMedia("
239 			+ app->javaScriptClass() + "," + jsRef() + ");");
240   }
241 }
242 
createDomElement(WApplication * app)243 DomElement *WAbstractMedia::createDomElement(WApplication *app)
244 {
245   loadJavaScript();
246 
247   DomElement *result = nullptr;
248 
249   if (isInLayout()) {
250     // It's easier to set WT_RESIZE_JS after the following code,
251     // but if it's not set, the alternative content will think that
252     // it is not included in a layout manager. Set some phony function
253     // now, which will be overwritten later in this method.
254     setJavaScriptMember(WT_RESIZE_JS, "function() {}");
255   }
256 
257   if (app->environment().agentIsIElt(9)) {
258     // Shortcut: IE misbehaves when it encounters a media element
259     result = DomElement::createNew(DomElementType::DIV);
260     if (alternative_)
261       result->addChild(alternative_->createSDomElement(app));
262   } else {
263     DomElement *media = createMediaDomElement();
264     DomElement *wrap = nullptr;
265     if (isInLayout()) {
266       media->setProperty(Property::StylePosition, "absolute");
267       media->setProperty(Property::StyleLeft, "0");
268       media->setProperty(Property::StyleRight, "0");
269       wrap = DomElement::createNew(DomElementType::DIV);
270       wrap->setProperty(Property::StylePosition, "relative");
271     }
272     result = wrap ? wrap : media;
273     if (wrap) {
274       mediaId_ = id() + "_media";
275       media->setId(mediaId_);
276     } else {
277       mediaId_ = id();
278     }
279 
280     updateMediaDom(*media, true);
281     // Create the 'source' elements
282     for (std::size_t i = 0; i < sources_.size(); ++i) {
283       DomElement *src = DomElement::createNew(DomElementType::SOURCE);
284       src->setId(mediaId_ + "s" + std::to_string(i));
285       renderSource(src, *sources_[i], i + 1 >= sources_.size());
286       media->addChild(src);
287     }
288     sourcesRendered_ = sources_.size();
289     sourcesChanged_ = false;
290 
291     if (wrap) {
292       wrap->addChild(media);
293     }
294   }
295 
296   if (isInLayout()) {
297     std::stringstream ss;
298     ss <<
299       """function(self, w, h) {";
300     if (!mediaId_.empty()) {
301       ss <<
302         ""  "v=" + jsMediaRef() + ";"
303         ""  "if (v) {"
304 	""    "if (w >= 0) "
305         ""      "v.setAttribute('width', w);"
306         ""    "if (h >= 0) "
307 	""      "v.setAttribute('height', h);"
308         ""  "}";
309     }
310     if (alternative_) {
311       ss <<
312         """a=" + alternative_->jsRef() + ";"
313         ""  "if (a && a." << WT_RESIZE_JS <<")"
314         ""    "a." << WT_RESIZE_JS << "(a, w, h);";
315     }
316     ss
317       <<"}";
318     setJavaScriptMember(WT_RESIZE_JS, ss.str());
319   }
320 
321   setId(result, app);
322   updateDom(*result, true);
323 
324   if (isInLayout()) {
325     result->setEvent(PLAYBACKSTARTED_SIGNAL, std::string());
326     result->setEvent(PLAYBACKPAUSED_SIGNAL, std::string());
327     result->setEvent(ENDED_SIGNAL, std::string());
328     result->setEvent(TIMEUPDATED_SIGNAL, std::string());
329     result->setEvent(VOLUMECHANGED_SIGNAL, std::string());
330   }
331 
332   setJavaScriptMember("mediaId", "'" + mediaId_ + "'");
333 
334   return result;
335 }
336 
jsMediaRef()337 std::string WAbstractMedia::jsMediaRef() const
338 {
339   if (mediaId_.empty()) {
340     return "null";
341   } else {
342     return WT_CLASS ".getElement('" + mediaId_ + "')";
343   }
344 }
345 
getDomChanges(std::vector<DomElement * > & result,WApplication * app)346 void WAbstractMedia::getDomChanges(std::vector<DomElement *>& result,
347 				   WApplication *app)
348 {
349   if (!mediaId_.empty()) {
350     DomElement *media = DomElement::getForUpdate(mediaId_, DomElementType::DIV);
351     updateMediaDom(*media, false);
352     if (sourcesChanged_) {
353       // Updating source elements seems to be ill-supported in at least FF,
354       // so we delete them all and reinsert them.
355       // Delete source elements that are no longer required
356       for (std::size_t i = 0; i < sourcesRendered_; ++i)
357 	media->callJavaScript
358 	  (WT_CLASS ".remove('" + mediaId_ + "s" + std::to_string(i) + "');",
359 	   true);
360       sourcesRendered_ = 0;
361       for (std::size_t i = 0; i < sources_.size(); ++i) {
362         DomElement *src = DomElement::createNew(DomElementType::SOURCE);
363         src->setId(mediaId_ + "s" + std::to_string(i));
364         renderSource(src, *sources_[i], i + 1 >= sources_.size());
365         media->addChild(src);
366       }
367       sourcesRendered_ = sources_.size();
368       sourcesChanged_ = false;
369       // Explicitly request rerun of media selection algorithm
370       // 4.8.9.2 says it should happen automatically, but FF doesn't
371       media->callJavaScript(jsMediaRef() + ".load();");
372     }
373     result.push_back(media);
374   }
375   WInteractWidget::getDomChanges(result, app);
376 }
377 
setOptions(const WFlags<PlayerOption> & flags)378 void WAbstractMedia::setOptions(const WFlags<PlayerOption>& flags)
379 {
380   flags_ = flags;
381   flagsChanged_ = true;
382   repaint();
383 }
384 
getOptions()385 WFlags<PlayerOption> WAbstractMedia::getOptions() const
386 {
387   return flags_;
388 }
389 
setPreloadMode(MediaPreloadMode mode)390 void WAbstractMedia::setPreloadMode(MediaPreloadMode mode)
391 {
392   preloadMode_ = mode;
393   preloadChanged_ = true;
394   repaint();
395 }
396 
preloadMode()397 MediaPreloadMode WAbstractMedia::preloadMode() const
398 {
399   return preloadMode_;
400 }
401 
clearSources()402 void WAbstractMedia::clearSources()
403 {
404   sources_.clear();
405   repaint();
406 }
407 
addSource(const WLink & link,const std::string & type,const std::string & media)408 void WAbstractMedia::addSource(const WLink& link, const std::string &type,
409 			       const std::string& media)
410 {
411   sources_.push_back
412     (std::unique_ptr<Source>(new Source(this, link, type, media)));
413   sourcesChanged_ = true;
414   repaint();
415 }
416 
setAlternativeContent(std::unique_ptr<WWidget> alternative)417 void WAbstractMedia::setAlternativeContent(std::unique_ptr<WWidget> alternative)
418 {
419   manageWidget(alternative_, std::move(alternative));
420 }
421 
iterateChildren(const HandleWidgetMethod & method)422 void WAbstractMedia::iterateChildren(const HandleWidgetMethod& method) const
423 {
424   if (alternative_)
425 #ifndef WT_TARGET_JAVA
426     method(alternative_.get());
427 #else
428     method.handle(alternative_.get());
429 #endif
430 }
431 
enableAjax()432 void WAbstractMedia::enableAjax()
433 {
434   WWebWidget::enableAjax();
435 
436   if (flags_.test(PlayerOption::Autoplay)) {
437     // chrome stops playing as soon as the widget tree is changed
438     // We therefore restart the play manually
439     play();
440   }
441 }
442 
Source(WAbstractMedia * parent,const WLink & link,const std::string & type,const std::string & media)443 WAbstractMedia::Source::Source(WAbstractMedia *parent,
444 			       const WLink& link, const std::string &type,
445 			       const std::string &media)
446   :  parent(parent),
447      type(type),
448      media(media),
449      link(link)
450 {
451   if (link.type() == LinkType::Resource) {
452     /*
453     connection = link.resource()->dataChanged().connect
454       ([=]() { this->resourceChanged(); });
455     */
456 #ifdef WT_TARGET_JAVA
457     connection = link.resource()->dataChanged().connect
458       (this, std::bind(&Source::resourceChanged, this));
459 #else // !WT_TARGET_JAVA
460     connection = link.resource()->dataChanged().connect
461       (std::bind(&Source::resourceChanged, this));
462 #endif // WT_TARGET_JAVA
463   }
464 }
465 
~Source()466 WAbstractMedia::Source::~Source()
467 {
468   connection.disconnect();
469 }
470 
resourceChanged()471 void WAbstractMedia::Source::resourceChanged()
472 {
473   parent->sourcesChanged_ = true;
474   parent->repaint();
475 }
476 
477