How to develop your own 'Delegate'
What is a delegate?
A delegate is a JavaScript Object that links the "Scene" and the "Player".
This object listens to the Player to interact with the Scene, and it listens to the Scene to interact with the Player.
It is an indispensable object, to connect the final Interactive Experience to the player.
What does need a delegate?
A delegate needs a Player API to know which event to listen, and which method to call.
More the Player API is complete, more the user experience should be better.
For a better experience:
Player's methods (or equivalent in terms of the player concerned):
- play()
- pause()
- seekTo()
- setVolume()
- mute/unmute()
- enterFullscreen/exitFullscreen()
- getDuration()
- getVolume()
- getMuted()
- getStreamSize() (or getStreamRatio())
- getPlayerSize() (getRendererSize())
- getStreamPosition() (position relative to the player/renderer)
- getCurrentTime()
- getContainer() (or directly the DOM Element where the player is rendered)
Player's events (or equivalent in terms of the player concerned):
- 'ready'
- 'playing'
- 'paused'
- 'currentTimeChanged'
- 'volumeChanged'
- 'muted/unmuted'
- 'seeking'
- 'seeked'
- 'ended'
- 'error'
- 'mediaChanged'
- 'streamSizeChanged'
- 'playerSizeChanged' ('rendererSizeChanged')
- 'streamPositionChanged'
Nb: if the player API object has a 'getContainer' which gives a DOM Element method with real sizes, we can listen this element to get the RendererSizes and the RenderezSizes when changed.
So the getPlayerSize and the 'playerSizeChanged' event may become useless.
The minimum to run the Interactivity being:
Player's methods:
- play()
- pause()
- seekTo()
- getCurrentTime (if the 'currentTime' event is not available)
- getContainer()
Player's events:
- 'ready'
- 'playing'
- 'paused'
- 'currentTime' ('progressTime')
Nb: with this minimum stuff, we can use the 'getContainer' method to get sizes. Considering the stream has same sizes than the player/renderer sizes.
In any case, it is required that a delegate reacts to :
- adways.hv.events.LAYER_ADDED
and defines :
- 'p2s.setStreamSize()'
- 'p2s.setStreamTransform()'
- 'p2s.setRenderSize()'
- 'p2s.setCurrentTime()'
- 'p2s.setDuration()' (for a better experience)
How to use the delegate to test it?
With the Experience Class, it's possible to pass your own delegate in order to test the delegate with the player concerned.
Here is an example to specify the HTML5 Video Delegate (developed by Adways) to an 'Experience' Object:
<script type="text/javascript" src="//dj5ag5n6bpdxo.cloudfront.net/libs/interactive/loader.js"></script> <script type="text/javascript"> var experience = new adways.interactive.Experience(); experience.setPlayerAPI(document.getElementsByTagName('video')[0]); // getting the video DOM Element to use the JavaScript API experience.setDelegateBuilderURL('//dj5ag5n6bpdxo.cloudfront.net/libs/delegateBuilders/htmlvideo.js'); experience.setDelegateBuilderClassname('adways.playerHelpers.HTMLVideoDelegateBuilder'); experience.setPublicationID('mpNHyTH'); experience.load(); </script>
This sample will load the publication "mpNHyTH", on top of a HTML5 Video Player, with the Delegate built by the "adways.playerHelpers.HTMLVideoDelegateBuilder" function.
Sample Delegate for HTML5 native video player, with comments
We are going to see, step by step, how to develop a delegate, based on the HTML5 native video player
There is no interface to develop a delegate, it's up to the developer to make it functional ;)
// we check if the Delegate is not already defined if (typeof HTMLVideoDelegate != "function") { // the constructor needs at least "Player2Scene SceneControllerWrapper" (P2S) // For more informations see this section // and "Scene2Player SceneControllerWrapper" (S2P) to communicate with the interactive engine. // And the JavaScript Player Object to communicate with the player API. HTMLVideoDelegate = function(p2s, s2p, playerAPI) { // we keep each arguments to local attributes this.playerAPI = playerAPI; this.p2s = p2s; this.s2p = s2p; // the default base z-index for layers which will receive the enrichments this.zIndexLayers = 1; // the 3 default DOM Elements: "Layers" this.hotspotDiv = null; this.controllerDiv = null; this.popupDiv = null; var that = this; // callback functions called by the different events attached to the playerAPI this.videoDurationListener = function() { // when receiving this event, we notify the P2S the new duration number if(that.playerAPI.duration!==0) that.p2s.setDuration(that.playerAPI.duration); }; // we call this callback once at the beginning to initialize the duration this.videoDurationListener(); // we can use the same protocol for each callback // link: "setter" on the scene with a "getter" from the player // (duration, currentTime, volume, mute, etc...) this.videoPlaybackRateListener = function() { that.p2s.setPlaybackRate(that.playerAPI.playbackRate); }; this.videoPlaybackRateListener(); this.videoTimeUpdateListener = function() { that.p2s.setCurrentTime(that.playerAPI.currentTime); }; this.videoTimeUpdateListener(); this.videoPlayStateListener = function() { if (that.playerAPI.paused) that.p2s.pause(); else that.p2s.play(); }; this.videoPlayStateListener(); this.isSeeking = false; this.videoSeekingListener = function() { if (!that.isSeeking) { that.isSeeking = true; that.savePlayState = that.playerAPI.paused; that.p2s.pause(); } }; this.videoSeekedListener = function() { that.isSeeking = false; if (that.savePlayState) that.p2s.pause(); else that.p2s.play(); if (!that.savePlayState) that.p2s.play(); }; this.videoEndedListener = function() { if (that.playerAPI.ended) that.p2s.end(); }; this.videoEndedListener(); this.videoVolumeListener = function() { if (that.playerAPI.muted) that.p2s.mute(); else that.p2s.unmute(); that.p2s.setVolume(that.playerAPI.volume); }; this.videoVolumeListener(); // this.sizeWatcherListener = function() { if (that.p2s.setRenderSize(new Array(that.playerAPI.offsetWidth, that.playerAPI.offsetHeight)) > 0) { that.updateStreamRect(); } that.updateLayersPosition(); }; this.sizeWatcherListener(); this.videoFullscreenListener = function() { // with the HTML5 player case, Scene Layers are not embed with the player when entering fullscreen // var fullscreenElement = document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement; if (fullscreenElement === that.playerAPI.parentNode) that.p2s.enterFullscreen(); else that.p2s.exitFullscreen(); }; // we notify the P2S what the player is able to change this.p2s.setCanChangeVolume(true); this.p2s.setCanChangeFullscreen(true); this.p2s.setCanChangePlaybackRate(true); this.p2s.setCanChangePlayState(true); this.p2s.setCanChangeCurrentTime(true); // we attach to the playerAPI all events available we need for the engine adways.misc.html.addEventListener(this.playerAPI, "durationchange", this.videoDurationListener, true); adways.misc.html.addEventListener(this.playerAPI, "ratechange", this.videoPlaybackRateListener, true); adways.misc.html.addEventListener(this.playerAPI, "timeupdate", this.videoTimeUpdateListener, true); adways.misc.html.addEventListener(this.playerAPI, "play", this.videoPlayStateListener, true); adways.misc.html.addEventListener(this.playerAPI, "pause", this.videoPlayStateListener, true); adways.misc.html.addEventListener(this.playerAPI, "ended", this.videoEndedListener, true); adways.misc.html.addEventListener(this.playerAPI, "seeking", this.videoSeekingListener, true); adways.misc.html.addEventListener(this.playerAPI, "seeked", this.videoSeekedListener, true); adways.misc.html.addEventListener(this.playerAPI, "volumechange", this.videoVolumeListener, true); adways.misc.html.addEventListener(document, "fullscreenchange", this.videoFullscreenListener, true); adways.misc.html.addEventListener(document, "mozfullscreenchange", this.videoFullscreenListener, true); adways.misc.html.addEventListener(document, "webkitfullscreenchange", this.videoFullscreenListener, true); adways.misc.html.addEventListener(document, "msfullscreenchange", this.videoFullscreenListener, true); // we attach to the S2P all events available we need for the player // see adways.resource.events namespace for more informations this.s2p.addEventListener(adways.resource.events.PLAY_STATE_CHANGED, this.s2pPlayStateChangedListener, this); this.s2p.addEventListener(adways.resource.events.VOLUME_CHANGED, this.s2pVolumeChangedListener, this); this.s2p.addEventListener(adways.resource.events.MUTED_CHANGED, this.s2pMutedChangedListener, this); this.s2p.addEventListener(adways.resource.events.CURRENT_TIME_CHANGED, this.s2pCurrentTimeChangedListener, this); this.s2p.addEventListener(adways.resource.events.PLAYBACK_RATE_CHANGED, this.s2pPlaybackRateChangedListener, this); this.s2p.addEventListener(adways.resource.events.FULLSCREEN_CHANGED, this.s2pFullscreenChangedListener, this); this.s2p.addEventListener(adways.hv.events.LAYER_ADDED, this.s2pLayerAddedListener, this); this.s2p.addEventListener(adways.hv.events.LAYER_REMOVED, this.s2pLayerRemovedListener, this); // as we don't have an event to listen the resize of the player // we launch a timer to check every 250ms the Player/renderer sizes this.sizeWatcherTimer = setInterval(this.sizeWatcherListener, 250); // at the beginning, if the scene already has its layers // we call the "s2pAddLayer" method to create the DOM Elements. // otherwise, the callback "s2pLayerAddedListener" will be called when LAYER_ADDED event is dispatched var layers = this.s2p.layersToArray(); for (var i = 0; i < layers.length; ++i) { this.s2pAddLayer(layers[i]); } }; // called when the Experience is destroyed, from the clearAll() method for example HTMLVideoDelegate.prototype.destroy = function() { // we detach from the S2P all events available we needed for the player this.s2p.removeEventListener(adways.resource.events.PLAY_STATE_CHANGED, this.s2pPlayStateChangedListener, this); this.s2p.removeEventListener(adways.resource.events.VOLUME_CHANGED, this.s2pVolumeChangedListener, this); this.s2p.removeEventListener(adways.resource.events.MUTED_CHANGED, this.s2pMutedChangedListener, this); this.s2p.removeEventListener(adways.resource.events.CURRENT_TIME_CHANGED, this.s2pCurrentTimeChangedListener, this); this.s2p.removeEventListener(adways.resource.events.PLAYBACK_RATE_CHANGED, this.s2pPlaybackRateChangedListener, this); this.s2p.removeEventListener(adways.resource.events.FULLSCREEN_CHANGED, this.s2pFullscreenChangedListener, this); this.s2p.removeEventListener(adways.hv.events.LAYER_ADDED, this.s2pLayerAddedListener, this); this.s2p.removeEventListener(adways.hv.events.LAYER_REMOVED, this.s2pLayerRemovedListener, this); // we detach from the playerAPI all events available we needed for the P2S adways.misc.html.removeEventListener(this.playerAPI, "durationchange", this.videoDurationListener, true); adways.misc.html.removeEventListener(this.playerAPI, "ratechange", this.videoPlaybackRateListener, true); adways.misc.html.removeEventListener(this.playerAPI, "timeupdate", this.videoTimeUpdateListener, true); adways.misc.html.removeEventListener(this.playerAPI, "play", this.videoPlayStateListener, true); adways.misc.html.removeEventListener(this.playerAPI, "pause", this.videoPlayStateListener, true); adways.misc.html.removeEventListener(this.playerAPI, "ended", this.videoEndedListener, true); adways.misc.html.removeEventListener(this.playerAPI, "seeking", this.videoSeekingListener, true); adways.misc.html.removeEventListener(this.playerAPI, "seeked", this.videoSeekedListener, true); adways.misc.html.removeEventListener(this.playerAPI, "volumechange", this.videoVolumeListener, true); adways.misc.html.removeEventListener(document, "fullscreenchange", this.videoFullscreenListener, true); adways.misc.html.removeEventListener(document, "mozfullscreenchange", this.videoFullscreenListener, true); adways.misc.html.removeEventListener(document, "webkitfullscreenchange", this.videoFullscreenListener, true); adways.misc.html.removeEventListener(document, "msfullscreenchange", this.videoFullscreenListener, true); // clearing the interval for the size watcher clearInterval(this.sizeWatcherTimer); }; // the scene notify the player its play state HTMLVideoDelegate.prototype.s2pPlayStateChangedListener = function() { switch (this.s2p.getPlayState().valueOf()) { // PLAYING case from the adways.resource.playStates namespace, so the method "play" of the player is called case adways.resource.playStates.PLAYING: this.playerAPI.play(); break; // PAUSED case from the adways.resource.playStates namespace, so the method "pause" of the player is called case adways.resource.playStates.PAUSED: this.playerAPI.pause(); break; } }; // the scene changed, so we notify the player its volume has changed // we can use the same protocol for each callback // link: "setter" on the player with a "getter" from the scene // (duration, currentTime, volume, mute, etc...) HTMLVideoDelegate.prototype.s2pVolumeChangedListener = function() { if (!isNaN(this.s2p.getVolume().valueOf())) this.playerAPI.volume = this.s2p.getVolume().valueOf(); }; HTMLVideoDelegate.prototype.s2pMutedChangedListener = function() { this.playerAPI.muted = this.s2p.getMuted().valueOf(); }; HTMLVideoDelegate.prototype.s2pCurrentTimeChangedListener = function() { if (!isNaN(this.s2p.getCurrentTime().valueOf())) this.playerAPI.currentTime = this.s2p.getCurrentTime().valueOf(); }; HTMLVideoDelegate.prototype.s2pPlaybackRateChangedListener = function() { if (!isNaN(this.s2p.getPlaybackRate().valueOf())) this.playerAPI.playbackRate = this.s2p.getPlaybackRate().valueOf(); }; HTMLVideoDelegate.prototype.s2pFullscreenChangedListener = function() { if (this.s2p.getFullscreen().valueOf()) { var element = this.playerAPI.parentNode; if (element.requestFullscreen) element.requestFullscreen(); else if (element.mozRequestFullscreen) element.mozRequestFullscreen(); else if (element.webkitRequestFullscreen) { element.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT); if (!document.webkitCurrentFullScreenElement) element.webkitRequestFullScreen(); } else if (element.msRequestFullscreen) element.msRequestFullscreen(); } else { if (document.exitFullscreen) document.exitFullscreen(); else if (document.cancelFullscreen) document.cancelFullscreen(); else if (document.mozExitFullscreen) document.mozExitFullscreen(); else if (document.mozCancelFullscreen) document.mozCancelFullscreen(); else if (document.webkitExitFullscreen) document.webkitExitFullscreen(); else if (document.webkitCancelFullscreen) document.webkitCancelFullscreen(); else if (document.msExitFullscreen) document.msExitFullscreen(); else if (document.msCancelFullscreen) document.msCancelFullscreen(); } }; // this callback notify the player a layer has to be added // "evt" contains a layer object from "getLayer" method, with the DOM element from the "getDomElement" method HTMLVideoDelegate.prototype.s2pLayerAddedListener = function(evt) { this.s2pAddLayer(evt.getLayer()); }; HTMLVideoDelegate.prototype.s2pAddLayer = function(layer) { var d = layer.getDomElement(); // absolute style on the DOM Element to prevent position issue d.style.position = "absolute"; // getting the parent node to append the layer var parent = this.video.offsetParent; if (parent === null) parent = this.video.parentNode; // we need to check which kind of layer we got to place them in the right order // Hotspot layer: behind others layers, end behind the native controller DOM Element of the player (if possible) // Popup layer: on top of others layers and the controller DOM Element of the player. // Up to the developers to place it on top of all player elements. // Controller layer: on top of the Hotspot layer, behind the Popup layer. if (layer.getLayerId() == adways.hv.layerIds.HOTSPOT) { this.hotspotDiv = d; this.hotspotDiv.style.zIndex = this.zIndexLayers; if (this.controllerDiv != null) parent.insertBefore(d, this.controllerDiv); else parent.appendChild(d); } else if (layer.getLayerId() == adways.hv.layerIds.CONTROLLER) { this.controllerDiv = d; this.controllerDiv.style.zIndex = this.zIndexLayers + 1; if (this.popupDiv != null) parent.insertBefore(d, this.popupDiv); else parent.appendChild(d); } else if (layer.getLayerId() == adways.hv.layerIds.POPUP) { this.popupDiv = d; this.popupDiv.style.zIndex = this.zIndexLayers + 2; parent.appendChild(d); } else parent.appendChild(d); this.updateLayersPosition(); }; // method called to replace the layers if needed (player resize, playlist panel for example...) HTMLVideoDelegate.prototype.updateLayersPosition = function() { // getting already added layers var layers = this.s2p.layersToArray(); for (var i = 0; i < layers.length; ++i) { var d = layers[i].getDomElement(); d.style.top = (this.playerAPI.offsetTop/* +top*/) + "px"; d.style.left = (this.playerAPI.offsetLeft/*+left*/) + "px"; } }; // callback to remove a layer by id (adways.hv.layerIds namespace) HTMLVideoDelegate.prototype.s2pLayerRemovedListener = function(evt) { var layer = evt.getLayer(); var d = layer.getDomElement(); d.parentNode.removeChild(d); switch (layer.getLayerId()) { case adways.hv.layerIds.HOTSPOT: this.hotspotDiv = null; break; case adways.hv.layerIds.POPUP: this.popupDiv = null; break; case adways.hv.layerIds.CONTROLLER: this.controllerDiv = null; break; } }; // The engine need the stream sizes and the stream position inside the renderer // this method has to call "setStreamSize" and "setStreamTransform". // We can use adways.geom.evaluateContentRect (as a helper) See adways.geom HTMLVideoDelegate.prototype.updateStreamRect = function() { if (!isNaN(this.playerAPI.videoWidth) && !isNaN(this.playerAPI.videoHeight) && !isNaN(this.playerAPI.offsetWidth) && !isNaN(this.playerAPI.offsetHeight)) { var streamSize = new Array(this.playerAPI.videoWidth, this.playerAPI.videoHeight); var tmp = adways.geom.evaluateContentRect(streamSize, new Array(this.playerAPI.offsetWidth, this.playerAPI.offsetHeight)); this.p2s.setStreamSize(new Array(tmp[2], tmp[3])); this.p2s.setStreamTransform(new Array(1, 0, 0, 1, tmp[0], tmp[1])); } else { if (!isNaN(this.playerAPI.videoWidth) && !isNaN(this.playerAPI.videoHeight)) { console.warn("Not able to determine the container size, positions and sizes may be wrong"); this.p2s.setStreamSize(new Array(this.playerAPI.videoWidth, this.playerAPI.videoHeight)); } else { console.warn("Not able to determine the native stream size, positions and sizes may be wrong"); this.p2s.setStreamSize(new Array(this.playerAPI.offsetWidth, this.playerAPI.offsetHeight)); // we can set (NaN,NaN) but setting the offsets is preferable } this.p2s.setStreamTransform(new Array(1, 0, 0, 1, 0, 0)); } }; } // on this sample, we place the DelegateBuilder at the end of the Delegate, in the same file. // we can imagine to append (to the body) the Delegate Script fom the DelegateBuilder to separate the files (DelegateBuilder.js / Delegate.js). // So if the user sets -on the Experience object- "setDelegateBuilderURL" with the URL of this file, // and "setDelegateBuilderClassname" with "adways.playerHelpers.HTMLVideoDelegateBuilder" // this delegate will be built and plugged to the player given adways.playerHelpers.HTMLVideoDelegateBuilder = function(experience) { var delegate = new HTMLVideoDelegate(experience.getP2S(), experience.getS2P(), experience.getPlayerAPI()); experience.delegateReady(delegate); };