OSMF HLS Plugin

Playback Apple's HLS VOD, Live and DVR streams in OSMF based players.

OSMF HLS Plugin Documentation

Plugin for OSMF based players, which provide capabilities to playback Apple's HLS format M3U8 files.

About

OSMF HLS Plugin — is a plugin for OSMF based players, which provide capabilities to playback Apple’s HLS format M3U8 files.

Features

  • Allows Playback of Apple’s HLS Format in OSMF based player;
  • Works with Multiple Qualities (Multi-Bitrate);
  • Supports VOD, Live and DVR streams;
  • Decodes H.264 Video and AAC / MP3 Audio;
  • Decrypt AES-128 / SAMPLE-AES Encrypted streams;
  • Alternate audio streams support (only VOD);
  • Handle ID3v2.X tags in custom player.

M3U8 Tags supported

  • #EXTINF
  • #EXTM3U
  • #EXT-X-DISCONTINUITY
  • #EXT-X-ENDLIST
  • #EXT-X-KEY
  • #EXT-X-MEDIA (only for VOD with TYPE=AUDIO, DEFAULT=NO and AUTOSELECT=NO)
  • #EXT-X-MEDIA-SEQUENCE
  • #EXT-X-STREAM-INF (with required BANDWIDTH and optional but recommended RESOLUTION parameters)
  • #EXT-X-TARGETDURATION

Plugin Limitations

  • Locked for one domain with unlimited subdomains;
  • JavaScript must be turned on;
  • Plugin will not playback in iframes, that added in other then locked domain (e.g. Facebook);
  • Minimum FlashPlayer version required is 10.2;
  • Minimum OSMF version for custom player is 2.0.

Simple Embed

First we start with importing swfobject.js in head section of the html file:

    <script type="text/javascript" src="http://yandex.st/swfobject/2.2/swfobject.min.js"></script>

Then add JavaScript code for embed SMP player on the page:

var flashvars = {
    // M3U8 url, or any other url which compatible with SMP player (flv, mp4, f4m)
    // escaped it for urls with ampersands
    src: escape("YOUR M3U8 URL HERE")
    // url to OSMF HLS Plugin
    , plugin_m3u8: "OSMFHLSPlugin.swf"
};
var params = {
    // self-explained parameters
    allowFullScreen: true
    , allowScriptAccess: "always"
    , bgcolor: "#000000"
};
var attrs = {
    name: "player"
};

swfobject.embedSWF(
    // url to SMP player
    "StrobeMediaPlayback.swf",
    // div id where player will be place
    "player",
    // width, height
    "800", "485",
    // minimum flash player version required
    "10.2",
    // other parameters
    null, flashvars, params, attrs
);

And finally, we add <div id="player"></div> in body section of the html file:

    <div id="player">
        <!-- this paragraph will be shown if FlashPlayer unavailable -->
        <p>
            <a href="http://www.adobe.com/go/getflashplayer">
                <img src="http://wwwimages.adobe.com/www.adobe.com/images/shared/download_buttons/get_flash_player.gif" alt="Get Adobe Flash player" />
            </a>
        </p>
    </div>

GTrack Plugin

First, complete previous section. After that, add this JavaScript code before flashvars to define GTrack configuration parameters:

// http://code.google.com/p/reops/wiki/GTrackPlugin
var gTrackPluginConfig = '<value key="reTrackConfig" type="class" class="com.realeyes.osmf.plugins.tracking.google.config.RETrackConfig"> \
    <account>YOUR GOOGLE ANALYTIC ID HERE</account> \
    <url>http://example.com</url> \
    <event name="percentWatched" category="video" action="percentWatched"> \
        <marker percent="0" label="start" /> \
        <marker percent="25" label="25PercentView" /> \
        <marker percent="50" label="50PercentView" /> \
        <marker percent="75" label="75PercentView" /> \
    </event> \
    <event name="complete" category="video" action="complete" value="1" /> \
    <event name="pageView" /> \
    <updateInterval>250</updateInterval> \
</value>';

Don’t forget to change account with your Google Analytic ID, and url.

This parameters said:

  • send tracking in category video with action percentWatched every 0%, 25%, 50% and 75% of watched video;
  • send tracking in category video with action complete and value 1 every time when video finished;
  • send flashvars.src as pageView when player loaded.

You can find more information about this and other parameters here.

And finally add parameters in flashvars:

var flashvars = {
    ...

    // Google Analytics settings

    // url to GTrack plugin
    , plugin_ga: "GTrackPlugin.swf"
    // pass parameters to GTrack plugin, which we defined above
    , "ga_http://www.realeyes.com/osmf/plugins/tracking/google": escape(gTrackPluginConfig)

    // uncomment next two lines if you want provide custom page url for tracking as "pageView" instead "flashvars.src"
    // , src_namespace_realeyes: "http://www.realeyes.com/osmf/plugins/tracking/google"
    // , src_realeyes_pageURL: "my_custom_page_url"

    ...
}

JavaScript Bridge

JavaScript Bridge allow us to communicate with player from javascript.

In flashvars add new parameter javascriptCallbackFunction with name of function in JavaScript, which will be invoke every time when player send standard events:

var flashvars = {
    ...
    , javascriptCallbackFunction: "onJSBridge"
    ...
}

Create this function with 3 parameters (playerId, event, obj) where:

  • playerId - we set this id in attrs variable here;
  • event - event name;
  • obj - data of that event.
var player;
function onJSBridge(playerId, event, obj) {
    switch(event) {
        case "onJavaScriptBridgeCreated":
            // reference to player
            player = document.getElementById(playerId);
            break;

        // other possible events
        case "emptied":
        case "loadstart":
        case "play":
        case "pause":
        case "waiting": // buffering
        case "loadedmetadata":
        case "seeking":
        case "seeked":
        case "volumechange":
        case "durationchange":
        case "timeupdate":
        case "progress": // for progressive download only
        case "complete":

        default:
            // console.log(event, obj);
            break;
    }
}

Inside this function you first get onJavaScriptBridgeCreated event when player ready.

Quality Switch

This section based on JavaScript Bridge section.

var player;
function onJSBridge(playerId, event, obj) {
    switch(event) {
        case "onJavaScriptBridgeCreated":
            ...

            // set autoSwitch to false
            // player.setAutoDynamicStreamSwitch(false);

            // events for dynamic streams
            player.addEventListener("isDynamicStreamChange", "onDynamicStream");
            player.addEventListener("switchingChange", "onDynamicStream");
            player.addEventListener("autoSwitchChange", "onDynamicStream");

            player.addEventListener("mediaSizeChange", "onDynamicStream");
            break;
    }
}

On onJavaScriptBridgeCreated event you subscribe on isDynamicStreamChange, switchingChange, autoSwitchChange and optional on mediaSizeChange events.

onDynamicStream is a JavaScript function which will be invoke from player. In this function you will create quality buttons and change it state (switching, switched). Also, i not recommend do any HTML DOM operations in this function, do all stuff in setTimeout, it prevent player from stuck:

function onDynamicStream() {
    setTimeout(updateDynamicStreamItems, 10);
}

function updateDynamicStreamItems() {
    var dssc = document.getElementById("dssc");
    var dynamicStreams = player.getStreamItems();
    dssc.style.display = dynamicStreams == null ? "none" : "block";
    if (dynamicStreams == null) return;
    var switchMode = player.getAutoDynamicStreamSwitch() ? "Auto" : "Manual";
    var isSwitching = player.getDynamicStreamSwitching();

    var dsItems = '<div id="switcher"><a href="#" onclick="player.setAutoDynamicStreamSwitch(!player.getAutoDynamicStreamSwitch()); return false;"> Switch Mode: ' + switchMode + '</a></div>';
    var currentStreamIndex = player.getCurrentDynamicStreamIndex();

    dsItems += '<ul id="rates">'
    var item;
    for (var idx = 0; idx < dynamicStreams.length; idx++) {
        var style = "";
        if (currentStreamIndex == idx) {
            style = isSwitching ? "switching" : "playing";
        }
        item = dynamicStreams[idx];
        dsItems += '<li class="' + style + '">'
            + '<a href="#" title="' + item.streamName + '"'
            + (currentStreamIndex != idx ? ' onclick="switchDynamicStreamIndex(' + idx + '); return false;"' : '')
            + '>'
            + Math.round(item.bitrate)
            + 'kbps'
            + (item.height > 0 ? ' <small>(' + item.height + 'p)</small>' : '')
            + '</a></li>';
    }
    dsItems += '</ul>'
    dssc.innerHTML = dsItems;
}

List elements will be rendered to "dssc" div element:

    <div id="player">
        ...
    </div>
    <div id="dssc"></div>

To change quality stream index you call player.switchDynamicStreamIndex(index) where:

  • player - you get this variable in onJSBridge;
  • index - you get in onDynamicStream with player.getStreamItems().
function switchDynamicStreamIndex(index) {
    if (player.getAutoDynamicStreamSwitch()) {
        player.setAutoDynamicStreamSwitch(false);
    }
    player.switchDynamicStreamIndex(index);
}

Alternate Audio-Streams

Limits:

  • Your main stream with video+audio is default, and all alternate audio-streams will replace main audio-stream “on the fly”;
  • DEFAULT and AUTOSELECT parameters of #EXT-X-MEDIA tag is always NO;

Example of M3U8 file with multi-bitrate video+audio streams, and 2 alternate audio-streams:

#EXTM3U
#EXT-X-MEDIA:URI="de_vod.m3u8",TYPE=AUDIO,LANGUAGE="de",NAME="German"
#EXT-X-MEDIA:URI="es_vod.m3u8",TYPE=AUDIO,LANGUAGE="es",NAME="Spanish"
#EXT-X-STREAM-INF:BANDWIDTH=481677,RESOLUTION=640x360
0440_vod.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1308077,RESOLUTION=640x360
1240_vod.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2650941,RESOLUTION=960x540
2540_vod.m3u8

This section based on JavaScript Bridge section.

Note: For this functionality you will need two more JavaScriptBridge events, which i provide to you with minor changes of default SMP player.

var player;
function onJSBridge(playerId, event, obj) {
    switch(event) {
        case "onJavaScriptBridgeCreated":
            player = document.getElementById(playerId);
            // events for alternative audio streams
            player.addEventListener("audioSwitchingChange", "onAlternativeAudioStream");
            player.addEventListener("numAlternativeAudioStreamsChange", "onAlternativeAudioStream");
            break;
    }
}

On onJavaScriptBridgeCreated event you subscribe on audioSwitchingChange and numAlternativeAudioStreamsChange events.

function onAlternativeAudioStream(isSwitching, playerId) {
    setTimeout(updateAlternativeAudioStreams, 10, isSwitching, playerId);
}

function updateAlternativeAudioStreams(isSwitching, playerId) {
    if (!playerId) {
        isSwitching = false;
    }

    var assc = document.getElementById("assc");
    var numAudioStreams = player.getNumAlternativeAudioStreams();
    if (numAudioStreams == 0) {
        assc.style.display = "none";
        return;
    } else {
        assc.style.display = "block";
    }
    var currentStreamIndex = player.getCurrentAlternativeAudioStreamIndex();

    asItems = '<ul id="audioStreams">';
    var item;
    for (var idx = -1; idx < numAudioStreams; idx++) {
        var style = "";
        if (currentStreamIndex == idx) {
            style = isSwitching ? "switching" : "playing";
        }
        item = player.getAlternativeAudioItemAt(idx);
        asItems += '<li class="' + style + '">'
            + '<a href="#"'
            + (currentStreamIndex != idx ? ' onclick="switchAlternateAudioStreamIndex(' + idx + '); return false;"' : '')
            + '>'
            + (idx == -1 ? 'Default' : item.info.label + ' (' + item.info.language + ')')
            + '</a></li>';
    }
    asItems += '</ul>'
    assc.innerHTML = asItems;
}

function switchAlternateAudioStreamIndex(index) {
    player.switchAlternativeAudioIndex(index);
}

List elements will be rendered to "assc" div element:

    <div id="player">
        ...
    </div>
    <div id="assc"></div>

How to create audio-only stream:

You can use ffmpeg, download it here.

@echo off
@set out_name=%~2

mkdir "%out_name%"

ffmpeg -i "%~1" -y -map 0 -c:a libvo_aacenc -f segment -segment_time 10 -segment_list "%out_name%.m3u8" "%out_name%/fileSequence%%d.aac"

Use it like this script.bat audio_source.wav audio_en where:

  • audio_source.wav is a source audio file
  • audio_en is a directory and playlist filename for generated chunks

Note: you must be sure that your video and audio playlists has accurate time for every chunks (e.g. not just 10 seconds, but 10.076), otherwise you will get synchronization problem on long time vods.

Load plugin in custom OSMF player

Be sure you use OSMF 2.0 version, you can get osmf.swc library here.

package {

    import flash.display.Sprite;

    import org.osmf.containers.MediaContainer;
    import org.osmf.events.MediaFactoryEvent;
    import org.osmf.media.DefaultMediaFactory;
    import org.osmf.media.MediaElement;
    import org.osmf.media.MediaFactory;
    import org.osmf.media.MediaPlayer;
    import org.osmf.media.URLResource;

    public class OSMFHLSPluginLoadExample extends Sprite {

        private var factory:MediaFactory;
        private var mediaPlayer:MediaPlayer;

        public function OSMFPluginLoadTest() {
            factory = new DefaultMediaFactory();
            factory.addEventListener(MediaFactoryEvent.PLUGIN_LOAD, onPluginLoad);
            factory.addEventListener(MediaFactoryEvent.PLUGIN_LOAD_ERROR, onPluginLoadError);
            // for local testing
            var resource:URLResource = new URLResource("file:///D:/PATH_TO_PLUGIN_SWF/OSMFHLSPlugin.swf");
            // for in-browser testing
            // var resource:URLResource = new URLResource("OSMFHLSPlugin.swf");
            factory.loadPlugin(resource);
        }

        private function onPluginLoad(event:MediaFactoryEvent):void {
            trace("The plug-in loaded successfully.");
            var resource:URLResource = new URLResource("vod.m3u8");
            // inject ext-x-key
            // resource.addMetadataValue("extXKey", 'METHOD=AES-128,URI="data:;base64,ST99RNbQvQrk8e4oi44RNw==",IV=0x8C8C4DF36B4A39BB106EDAB0D5D88960');
            var media:MediaElement = factory.createMediaElement(resource);
            mediaPlayer = new MediaPlayer();
            mediaPlayer.media = media;
            mediaPlayer.autoPlay = true;
            var mediaContainer:MediaContainer = new MediaContainer();
            mediaContainer.addMediaElement(media);
            addChild(mediaContainer);
        }

        private function onPluginLoadError(event:MediaFactoryEvent):void {
            trace("The plug-in failed to load.");
        }

    }

}

Embed plugin in custom OSMF player

Be sure you use OSMF 2.0 version, you can get osmf.swc library here.

package {

    import flash.display.Loader;
    import flash.display.Sprite;
    import flash.display.StageScaleMode;
    import flash.events.Event;
    import flash.system.ApplicationDomain;
    import flash.system.LoaderContext;
    import flash.utils.ByteArray;
    import flash.utils.getDefinitionByName;

    import org.osmf.containers.MediaContainer;
    import org.osmf.events.MediaFactoryEvent;
    import org.osmf.media.DefaultMediaFactory;
    import org.osmf.media.MediaElement;
    import org.osmf.media.MediaFactory;
    import org.osmf.media.MediaPlayer;
    import org.osmf.media.PluginInfoResource;
    import org.osmf.media.URLResource;

    [SWF(width=640, height=360, backgroundColor=0, frameRate=25)]

    public class OSMFHLSPluginEmbedExample extends Sprite {

        // embed plugin file inside custom player
        [Embed("../bin-debug/OSMFHLSPlugin_trial.swf", mimeType="application/octet-stream")]
        private static const OSMFHLSPluginData:Class;
        private static const OSMFHLSPlugin:ByteArray = new OSMFHLSPluginData();

        private var factory:MediaFactory;
        private var mediaPlayer:MediaPlayer;

        public function OSMFHLSPluginEmbedExample() {
            stage.scaleMode = StageScaleMode.NO_SCALE;
            // load plugin class inside custom player
            var l:Loader = new Loader();
            l.contentLoaderInfo.addEventListener(Event.COMPLETE, onEmbedPluginLoaded);
            var lc:LoaderContext = new LoaderContext(false, ApplicationDomain.currentDomain);
            lc.allowCodeImport = true;
            l.loadBytes(OSMFHLSPlugin, lc);
        }

        private function onEmbedPluginLoaded(event:Event):void {
            // instantiate plugin
            var PluginClass:Class = getDefinitionByName("ru.kutu.osmf.hls.OSMFHLSPlugin") as Class;
            var pluginInstance:Object = new PluginClass();
            var resource:PluginInfoResource = new PluginInfoResource(pluginInstance["pluginInfo"]);

            factory = new DefaultMediaFactory();
            factory.addEventListener(MediaFactoryEvent.PLUGIN_LOAD, onPluginLoad);
            factory.addEventListener(MediaFactoryEvent.PLUGIN_LOAD_ERROR, onPluginLoadError);
            factory.loadPlugin(resource);
        }

        private function onPluginLoad(event:MediaFactoryEvent):void {
            trace("The plug-in loaded successfully.");
            var resource:URLResource = new URLResource("vod.m3u8");
            var media:MediaElement = factory.createMediaElement(resource);
            mediaPlayer = new MediaPlayer();
            mediaPlayer.media = media;
            mediaPlayer.autoPlay = true;
            var mediaContainer:MediaContainer = new MediaContainer();
            mediaContainer.addMediaElement(media);
            addChild(mediaContainer);
        }

        private function onPluginLoadError(event:MediaFactoryEvent):void {
            trace("The plug-in failed to load.");
        }

    }

}

Integrate plugin in AIR application

Note: Plugin will not work on iOS devices.

Be sure you use OSMF 2.0 version, you can get osmf.swc library here.

<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
                       xmlns:s="library://ns.adobe.com/flex/spark"
                       width="640" height="360" showStatusBar="false"
                       applicationComplete="onApp()">

    <fx:Script>
        <![CDATA[
            import mx.core.mx_internal;

            import org.osmf.events.MediaFactoryEvent;
            import org.osmf.media.MediaFactory;
            import org.osmf.media.PluginInfoResource;

            import ru.kutu.osmf.hls.OSMFHLSPlugin;

            private function onApp():void {
                var factory:MediaFactory = player.videoDisplay.mx_internal::mediaFactory;
                factory.addEventListener(MediaFactoryEvent.PLUGIN_LOAD, onPluginLoad);
                factory.loadPlugin(new PluginInfoResource(new OSMFHLSPlugin().pluginInfo));
            }

            private function onPluginLoad(event:MediaFactoryEvent):void {
                player.source = "vod.m3u8";
            }
        ]]>
    </fx:Script>

    <s:VideoPlayer id="player" width="100%" height="100%" autoPlay="true" />

</s:WindowedApplication>

Handle ID3v2 Tags

Be sure you use OSMF 2.0 version, you can get osmf.swc library here.

For Base64 decode, this example utilize BlooDHounD library, you can download it here.

package {

    import by.blooddy.crypto.Base64;

    import flash.display.Sprite;
    import flash.display.StageAlign;
    import flash.display.StageScaleMode;
    import flash.net.NetStream;
    import flash.utils.ByteArray;

    import org.osmf.containers.MediaContainer;
    import org.osmf.elements.ProxyElement;
    import org.osmf.events.LoadEvent;
    import org.osmf.events.MediaElementEvent;
    import org.osmf.events.MediaFactoryEvent;
    import org.osmf.media.DefaultMediaFactory;
    import org.osmf.media.MediaElement;
    import org.osmf.media.MediaFactory;
    import org.osmf.media.MediaPlayer;
    import org.osmf.media.URLResource;
    import org.osmf.net.NetClient;
    import org.osmf.net.NetStreamLoadTrait;
    import org.osmf.traits.LoadState;
    import org.osmf.traits.MediaTraitType;

    [SWF(width=640, height=360, backgroundColor=0, frameRate=25)]

    public class OSMFHLSPluginID3 extends Sprite {

        private var factory:MediaFactory;
        private var mediaPlayer:MediaPlayer;

        public function OSMFHLSPluginID3() {
            stage.scaleMode = StageScaleMode.NO_SCALE;
            stage.align = StageAlign.TOP_LEFT;
            factory = new DefaultMediaFactory();
            factory.addEventListener(MediaFactoryEvent.PLUGIN_LOAD, onPluginLoad);
            factory.addEventListener(MediaFactoryEvent.PLUGIN_LOAD_ERROR, onPluginLoadError);
            // url for local file system load plugin
            var resource:URLResource = new URLResource("file:///D:/PATH_TO_PLUGIN_SWF/OSMFHLSPlugin.swf");
            // for in-browser testing
            // var resource:URLResource = new URLResource("OSMFHLSPlugin.swf");
            factory.loadPlugin(resource);
        }

        private function onPluginLoad(event:MediaFactoryEvent):void {
            trace("The plug-in loaded successfully.");
            var resource:URLResource = new URLResource("playlist.m3u8");
            var media:MediaElement = factory.createMediaElement(resource);
            // start listening traitAdd event
            media.addEventListener(MediaElementEvent.TRAIT_ADD, onTraitAdd);
            mediaPlayer = new MediaPlayer();
            mediaPlayer.media = media;
            mediaPlayer.autoPlay = true;
            var mediaContainer:MediaContainer = new MediaContainer();
            mediaContainer.addMediaElement(media);
            addChild(mediaContainer);
        }

        private function onPluginLoadError(event:MediaFactoryEvent):void {
            trace("The plug-in failed to load.");
        }

        private function onTraitAdd(event:MediaElementEvent):void {
            // got netStream load trait
            // and start listening loadState changing
            if (event.traitType == MediaTraitType.LOAD) {
                var media:MediaElement = mediaPlayer.media;
                media.removeEventListener(MediaElementEvent.TRAIT_ADD, onTraitAdd);

                while (media is ProxyElement && mediaPlayer.media) {
                    media = (mediaPlayer.media as ProxyElement).proxiedElement;
                }

                var lt:NetStreamLoadTrait = media.getTrait(MediaTraitType.LOAD) as NetStreamLoadTrait;
                if (lt) {
                    lt.addEventListener(LoadEvent.LOAD_STATE_CHANGE, onNetStreamLoadTraitStateChange);
                }
            }
        }

        private function onNetStreamLoadTraitStateChange(event:LoadEvent):void {
            // start listening onID3 in netStream client
            if (event.loadState == LoadState.READY) {
                var lt:NetStreamLoadTrait = event.target as NetStreamLoadTrait;
                lt.removeEventListener(LoadEvent.LOAD_STATE_CHANGE, onNetStreamLoadTraitStateChange);

                var ns:NetStream = lt.netStream;
                if (ns && ns.client) {
                    NetClient(ns.client).addHandler("onID3", onID3);
                }
            }
        }

        private function onID3(data:Object):void {
            // got ID3 tag
            // {
            //     TIT2: "...",
            //     TXXX: "...",
            //     ...
            //     APIC: "base64 encoded string"
            // }
            trace(mediaPlayer.currentTime);
            for (var k:String in data) {
                if (k.charAt(0) == "T") {
                    trace(k + ":", data[k]);
                } else {
                    var ba:ByteArray = Base64.decode(data[k]);
                    if (k == "APIC") {
                        trace(k, ba.length, data[k]);
                        // handle APIC tag, and save it to file
                        // var f:FileReference = new FileReference();
                        // f.save(ba, "apic.jpg");
                    }
                }
            }
        }

    }

}

Handle Error

Sometimes errors is happen, and you need to handle this to decide what to do:

  • send log
  • try to start stream again
  • try to start backup stream
// add listener to handle mediaError event
player.addEventListener("mediaError", "onMediaError");
...
function onMediaError(code, message, detail, playerId) {
    // here you can restart stream
    // player.load();

    // or start backup stream
    // player.setMediaResourceURL("backupUrl.m3u8");
}

Troubleshooting

Before investigate any problem, open Developer Tools in Chrome, Safari or Firefox and check "Network" tab on errors while download m3u8-playlist or ts-chunks.

My M3U8 url doesn’t have .m3u8 extension in path:

Add in flashvars mimeType: "application/vnd.apple.mpegurl".