Overview
The example shows how to convert stream published on WCS server to HLS and play it in browser using VideoJS library. HLS stream cut starts automatically when strea is requested by HLS URL, for example `https://test1.flashphoner.com:8445/test/test.m3u8` on the screenshot below.
The example allows to choose VideoJS version to play. VideoJS 8 supports Low Latency HLS if server allows to play this format
The code of the example
The source code can be accessed on server by the following path:
/usr/local/FlashphonerWebCallServer/client2/examples/demo/streaming/hls-player
hls-player.css - player page styles file
hls-player.html - player page
hls-player.js - player launch script
player-page.html - common player page elements for three HLS playback examples
The videojs7 and videojs8 subfolders contain two VideoJS versions respectively:
video.js - the player library script (http://videojs.com/, Apache License Version 2.0)
video.min.js - the player library script (minimized)
video-js.css - the player library styles file
The example can be tested using the fiollowing URL:
https://host:8444/client2/examples/demo/streaming/hls-player/hls-player.html
Where host is WCS server address
Analyzing the code
To analyze the code get hls-player.js file version with hash 32144d9, which is available here and can be downloaded in build 2.0.243.
1. Loading the player page
const loadPlayerPage = function() { if (videojsVersion) { hideItem("videojsInputForm"); loadVideoJS("videojs" + videojsVersion); } else { let videojsInput = document.getElementById("videojsInput"); for (videojsType in VIDEOJS_VERSION_TYPE) { let option = document.createElement("option"); let videojsFolder = ""; switch (videojsType) { case 'VIDEOJS7': videojsFolder = VIDEOJS_VERSION_TYPE.VIDEOJS7; break; case 'VIDEOJS8': videojsFolder = VIDEOJS_VERSION_TYPE.VIDEOJS8; break; } option.text = videojsFolder; option.value = videojsFolder; videojsInput.appendChild(option); } setHandler("videojsBtn", "click", onVideojsBtnClick); } }
2. Loading the VideoJS chosen version
const loadVideoJS = function (version) { if (version) { let playerPage = document.getElementById("playerPage"); loadFile(version + "/video.js", "text/javascript").then( data => { console.log("HLS library loaded successfully", data); loadFile(version + "/video-js.css", "stylesheet").then ( data => { console.log("HLS library stylesheet loaded successfully", data); hideItem("videojsInputForm"); loadPage("player-page.html", "playerPage", initPage ); }).catch( err => { playerPage.innerHTML = "Can't load VideoJS library stylesheet"; playerPage.setAttribute("class", "text-danger"); console.error(err); }) }).catch( err => { setText("videojsError", "Can't load VideoJS library"); console.error(err); }); } }
3. The player HTML page initializing
const initPage = function() { setText("header", "HLS VideoJS Player Minimal"); setValue("urlServer", getHLSUrl()); enableItem("applyBtn"); setText("applyBtn", "Play"); setHandler("applyBtn", "click", playBtnClick); setHandler("backBtn10", "click", backBtnClick); setHandler("backBtn30", "click", backBtnClick); setHandler("backBtnMax", "click", backBtnClick); setHandler("liveBtn", "click", liveBtnClick); let remoteVideo = document.getElementById('remoteVideo'); remoteVideo.className = "video-js vjs-default-skin"; player = initVideoJsPlayer(remoteVideo); playbackStats = PlaybackStats(STATS_INTERVAL); }
4. Player initializing
videojs() code
The following parameters are passed to the player:
- video - div tag to play the stream on the page
- playsinline: true - play a video on the page without automatic full screen switching (ignored in iOS Safari)
- playbackRates - playback rates list
- liveui: true - enables rewind (DVR) interface
- liveTracker - live playback thresholds setup
- fill: true - scale the player to div tag dimensions
const initVideoJsPlayer = function(video) { let videoJsPlayer = videojs(video, { playsinline: true, playbackRates: [0.1, 0.25, 0.5, 1, 1.5, 2], liveui: true, liveTracker: { trackingThreshold: LIVE_THRESHOLD, liveTolerance: LIVE_TOLERANCE }, fill: true }); console.log("Using VideoJs " + videojs.VERSION); if (Browser.isSafariWebRTC() && Browser.isiOS()) { // iOS hack when using standard controls to leave fullscreen mode let videoTag = getActualVideoTag(); if(videoTag) { setWebkitFullscreenHandlers(videoTag, false); } } return videoJsPlayer; }
5. Stream name detection (the stream should be published to server)
encodeURIComponent() code
function playBtnClick() { if (validateForm()) { let streamName = getValue('playStream'); streamName = encodeURIComponent(streamName); ... } }
6. HLS stream URL forming and player launching
player.play() code
If authentication key and token are set, they will be included to stream URL
const playBtnClick = function() { if (validateForm()) { ... let videoSrc = getValue("urlServer") + '/' + streamName + '/' + streamName + '.m3u8'; let key = getValue('key'); let token = getValue("token"); if (key.length > 0 && token.length > 0) { videoSrc += "?" + key + "=" + token; } player.on('loadedmetadata', function() { console.log("Play with VideoJs"); player.play(); }); ... player.src({ src: videoSrc, type: "application/vnd.apple.mpegurl" }); onStarted(); } }
7. playing
event handler
player.on() code
const playBtnClick = function() { if (validateForm()) { ... player.on('playing', function() { console.log("playing event fired"); if (player.liveTracker) { if (!player.liveTracker.isLive()) { // A cratch to display live UI for the first subscriber liveUIDisplay(); } if (player.liveTracker.atLiveEdge()) { // Unlock backward buttons when seeked to live edge toggleBackButtons(true); // Stop live UI startup timer stopLiveUITimer(); } } }); ... } }
8. Enable rewind (DVR) interface for the first subscriber
player.liveTracker.seekToLiveEdge() code
const liveUIDisplay = function() { stopLiveUITimer() if (player && player.liveTracker) { liveUITimer = setInterval(function() { if (!player.liveTracker.isLive() && player.liveTracker.liveWindow() > LIVE_THRESHOLD) { // Live UI is not displayed yet, seek to live edge to display player.liveTracker.seekToLiveEdge(); } }, LIVE_UI_INTERVAL) } }
9. Rewind button click action
player.seekable(), player.currentTime() code
const backBtnClick = function(event) { if (player != null && player.liveTracker) { toggleBackButtons(false); let seekable = player.seekable(); let backTime = -1; if (event.target.id.indexOf("10") !== -1) { backTime = player.currentTime() - 10; } else if (event.target.id.indexOf("30") !== -1) { backTime = player.currentTime() - 30; } if (backTime < 0) { backTime = seekable ? seekable.start(0) : player.currentTime(); } player.currentTime(backTime); } }
10. Live button click action
player.liveTracker.seekToLiveEdge() code
const liveBtnClick = function() { if (player != null && player.liveTracker) { player.liveTracker.seekToLiveEdge(); toggleBackButtons(true); } }
11. Playback stopping
player.dispose() code
This method removes the div container tag where player was initialized from the page
const stopBtnClick = function() { if (player != null) { console.log("Stop VideoJS player"); stopLiveUITimer(); player.dispose(); } onStopped(); }
12. New div container tag creation after previous player was removed
const createRemoteVideo = function(parent) { let remoteVideo = document.createElement("video"); remoteVideo.id = "remoteVideo"; remoteVideo.controls="controls"; remoteVideo.autoplay="autoplay"; remoteVideo.type="application/vnd.apple.mpegurl"; remoteVideo.className = "video-js vjs-default-skin"; remoteVideo.setAttribute("playsinline",""); remoteVideo.setAttribute("webkit-playsinline",""); parent.appendChild(remoteVideo); player = initVideoJsPlayer(remoteVideo); }
13. Getting an available playback statistics from HTML5 video tag
HTML5Stats code
const PlaybackStats = function(interval) { const playbackStats = { interval: interval || STATS_INTERVAL, timer: null, stats: null, start: function() { let video = getActualVideoTag(); playbackStats.stop(); stats = HTML5Stats(video); playbackStats.timer = setInterval(playbackStats.displayStats, playbackStats.interval); setText("videoWidth", "N/A"); setText("videoHeight", "N/A"); setText("videoRate", "N/A"); setText("videoFps", "N/A"); showItem("stats"); }, stop: function() { if (playbackStats.timer) { clearInterval(playbackStats.timer); playbackStats.timer = null; } playbackStats.stats = null; hideItem("stats"); }, displayStats: function() { if (stats.collect()) { let width = stats.getWidth(); let height = stats.getHeight(); let bitrate = stats.getBitrate(); let fps = stats.getFps(); setText("videoWidth", width); setText("videoHeight", height); if (bitrate !== undefined) { setText("videoRate", Math.round(bitrate)); } if (fps !== undefined) { setText("videoFps", fps.toFixed(1)); } } } }; return playbackStats; }