Order Now AdSolution Sign Up | Login » Bits on the Run Sign Up | Login »

Forums

/

Change Captions & Audiodescription Files

21 replies [Last post]

 
From time to time, I've seen someone requesting the ability to change captions and/or audiodescription files on-the-fly, to change languages, etc.

Seems it's easy to modify the captions and audiodescription plugins to load a new file anytime.

Seems to work … well … see for yourself:

    http://willswonders.myip.org:8074/player5/Simple_TitlePlugin_v5.html

    (Note: Loading the HD file from the playlist doesn't work yet.)

Have fun! Play with the buttons —    :D

Look at the page source for the modified plugins.

Here's the ActionScript — v4 & v5 compatible.

Audiodescription.as

/**
* Plugin for playing a closed audiodescription with a video.
**/
package com.jeroenwijering.plugins
{
  import com.jeroenwijering.events.*;
  import com.jeroenwijering.utils.Logger;

  import flash.display.*;
  import flash.events.*;
  import flash.media.*;
  import flash.net.*;

//...Will
  import flash.external.ExternalInterface;
  import flash.ui.Keyboard;  

  public class Audiodescription extends MovieClip implements PluginInterface
  {
    [Embed(source="../../../controlbar.png")]
    private const ControlbarIcon:Class;
    [Embed(source="../../../dock.png")]
    private const DockIcon:Class;

    /** Reference to the dock button. **/
    private var button:MovieClip;

    /** List with configuration settings. **/
    public var config:Object =
    {
      file:undefined,
      state:true,
      volume:90
    }

    /** Reference to the MVC view. **/
    private var view:AbstractView;
    /** Reference to the icon. **/
    private var icon:Bitmap;
    /** sound object to be instantiated. **/
    private var sound:Sound;
    /** Sound channel object. **/
    private var channel:SoundChannel;

    /** Constructor; not much going on. **/
    public function Audiodescription():void {};

    /** Initing the plugin. **/
    public function initializePlugin(vie:AbstractView):void
    {
      view = vie;
      view.addControllerListener(ControllerEvent.ITEM, itemHandler);
      view.addModelListener(ModelEvent.TIME,           timeHandler);
      view.addModelListener(ModelEvent.STATE,          stateHandler);

      if(view.config['dock'])
      {
        button = view.getPlugin('dock').addButton(DisplayObject(new DockIcon()), 'is on', clickHandler);
      }
      else if(view.getPlugin('controlbar'))
      {
        icon = new ControlbarIcon();
        view.getPlugin('controlbar').addButton(icon, 'audiodescription', clickHandler);
      }

      setState(config['state']);

//...Will
      if(ExternalInterface.available)
      {
        ExternalInterface.addCallback('changeAudioFile', changeAudioFile);
      }
    };

    /** Clicking the hide button. **/
    private function clickHandler(evt:MouseEvent):void
    {
      setState(!config['state']);
    };

    /** Check for audiodescription with a new item. **/
    private function itemHandler(evt:ControllerEvent = null):void
    {
      var file:String;

      if(view.playlist[view.config['item']]['audiodescription.file'])
      {
        file = view.playlist[view.config['item']]['audiodescription.file'];
      }
      else if(view.playlist[view.config['item']]['audiodescription'])
      {
        file = view.playlist[view.config['item']]['audiodescription']; 
      }
      else if(view.config['audiodescription.file'])
      {
        file = view.config['audiodescription.file'];
      }
      else if(view.config['audio'])
      {
        // Legacy support
        file = view.config['audio'];
      }

      if(file)
      {
        config['file'] = file;

        try
        {
          if(channel)
          {
            channel.stop();
          }

          sound = new Sound(new URLRequest(config['file']));
          channel = sound.play();
          setVolume();

//...Will - if player state is IDLE on startup, then stop the audio
//ExternalInterface.call("(function(input){alert('Player State: ' + input);})", view.config['state']);
          if((view.config['state'] == 'IDLE') && (channel))
          {
            channel.stop();
          }
        }
        catch (err:Error)
        {
          Logger.log(err.message, 'audiodescription');
        }
      }
    };

//...Will - change audiodescription file on-the-fly
    public function changeAudioFile(audiofile:String):void
    {
//ExternalInterface.call("(function(input){alert('New Audiodescription File: ' + input);})", audiofile);

      if(audiofile)
      {
        config['file'] = audiofile;

        try
        {
          if(channel)
          {
            channel.stop();
          }

          sound = new Sound(new URLRequest(config['file']));
          channel = sound.play();
          setVolume();

          if((view.config['state'] == 'IDLE') && (channel))
          {
            channel.stop();
          }
        }
        catch (err:Error)
        {
          Logger.log(err.message, 'audiodescription');
        }
      }
    };

    /** Turn the audiodescription on/off. **/
    public function setState(stt:Boolean):void
    {
      config['state'] = stt;
      var cke:SharedObject = SharedObject.getLocal('com.jeroenwijering','/');
      cke.data['audiodescription.state'] = stt;
      cke.flush();
      setVolume();

      if(stt)
      { 
        if(button)
        { 
          button.field.text = 'is on'; 
        }
        else
        {
          icon.alpha = 1;
        }
      }
      else
      {
        if(button)
        { 
          button.field.text = 'is off'; 
        }
        else
        {
          icon.alpha = 0.3;
        }
      }
    };

    /** Set the volume level. **/
    private function setVolume():void
    {
      var trf:SoundTransform = new SoundTransform(config['volume'] / 100);

      if(!config['state'])
      {
        trf.volume = 0;
      }

      if(channel)
      {
        channel.soundTransform = trf;
      }
    };

    /** The statehandler manages audio pauses. **/
    private function stateHandler(evt:ModelEvent):void
    {
//ExternalInterface.call("(function(input){alert('New State: ' + input);})", evt.data.newstate);
      switch(evt.data.newstate)
      {
        case ModelStates.PAUSED:
        case ModelStates.COMPLETED:
        case ModelStates.IDLE:
          if(channel)
          {
            channel.stop();
          }
          break;
      }
    };

    /** Check timing of the player to sync audio if needed. **/
    private function timeHandler(evt:ModelEvent):void
    {
      var pos:Number = evt.data.position;

      if(channel && view.config['state'] == ModelStates.PLAYING && Math.abs(pos-channel.position / 1000) > 0.5)
      {
        channel.stop();
        channel = sound.play(pos * 1000);
        setVolume();
      }
    };
  };
}

Captions.as

/*
 * Plugin for playing closed captions with a video.
 */
package com.jeroenwijering.plugins
{
  import com.jeroenwijering.events.*;
  import com.jeroenwijering.parsers.*;
  import com.jeroenwijering.utils.Logger;

  import flash.display.*;
  import flash.events.*;
  import flash.filters.DropShadowFilter;
  import flash.net.*;
  import flash.text.*;

//...Will
  import flash.external.ExternalInterface;
  import flash.ui.Keyboard;  

  public class Captions extends MovieClip implements PluginInterface
  {
    [Embed(source = '../../../controlbar.png')]
    private const ControlbarIcon:Class;
    [Embed(source = '../../../dock.png')]
    private const DockIcon:Class;

    /** List with configuration settings. **/
    public var config:Object =
    {
      back:false,
      file:undefined,
      fontsize:14,
      state:true,
//...Will
      listener:undefined
    };

    /** XML connect and parse object. **/
    private var loader:URLLoader;
    /** Reference to the MVC view. **/
    private var view:AbstractView;
    /** Icon for the controlbar. **/
    private var icon:Bitmap;
    /** Reference to the textfield. **/
    public var field:TextField;
    /** Reference to the background graphic. **/
    private var back:MovieClip;
    /** The array the captions are loaded into. **/
    private var captions:Array;
    /** Textformat entry for the captions. **/
    private var format:TextFormat;
    /** Currently active caption. **/
    private var current:Number;
    /** Reference to the dock button. **/
    private var button:MovieClip;

    public function Captions()
    {
      loader = new URLLoader();
      loader.addEventListener(Event.COMPLETE, loaderHandler);
    };

    /** Clicking the hide button. **/
    private function clickHandler(evt:MouseEvent):void
    {
      hide(!config['state']);
    };

    private function drawClip():void
    {
      back                    = new MovieClip();
      back.graphics.beginFill(0x000000, 0.75);
      back.graphics.drawRect(0, 0, 400, 20);
      addChild(back);
      format                  = new TextFormat();
      format.color            = 0xFFFFFF;
      format.size             = config['fontsize'];
      format.align            = 'center';
      format.font             = '_sans';
      format.leading          = 4;
      field                   = new TextField();
      field.width             = 400;
      field.height            = 10;
      field.y                 = 5;
      field.autoSize          = 'center';
      field.selectable        = false;
      field.multiline         = true;
      field.wordWrap          = true;
      field.defaultTextFormat = format;
      addChild(field);

      if(config['back'] == false)
      {
        back.alpha    = 0;
        field.filters = new Array(new DropShadowFilter(0, 45, 0, 1, 2, 2, 10, 3));
      }
    };

    /** Show/hide the captions **/
    public function hide(stt:Boolean):void
    {
      config['state'] = stt;
      visible         = config['state'];

      if(config['state'])
      {
        if(button)
        { 
          button.field.text = 'is on'; 
        }
        else
        { 
          icon.alpha = 1;
        }
      }
      else
      { 
        if(button)
        { 
          button.field.text = 'is off'; 
        }
        else
        {
          icon.alpha = 0.3;
        }
      }

      var cke:SharedObject = SharedObject.getLocal('com.jeroenwijering', '/');
      cke.data['captions.state'] = stt;
      cke.flush();
    };

    /** Initing the plugin. **/
    public function initializePlugin(vie:AbstractView):void
    {
      view = vie;
      view.addControllerListener(ControllerEvent.ITEM, itemHandler);
      view.addControllerListener(ControllerEvent.RESIZE, resizeHandler);
      view.addModelListener(ModelEvent.TIME, timeHandler);
      view.addModelListener(ModelEvent.STATE, stateHandler);
      view.addModelListener(ModelEvent.META, metaHandler);
      drawClip();
      mouseEnabled  = false;
      mouseChildren = false;

      if(view.config['dock'])
      {
        button = view.getPlugin('dock').addButton(new DockIcon(), 'is on', clickHandler);
      }
      else if(view.getPlugin('controlbar'))
      {
        icon = new ControlbarIcon();
        view.getPlugin('controlbar').addButton(icon, 'captions', clickHandler);
      }

//...Will
      if(ExternalInterface.available)
      {
        ExternalInterface.addCallback('toggleCaptions', toggleCaptions);
      }

//...Will
      stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDownHandler);

//...AM
      if(ExternalInterface.available)
      {
        ExternalInterface.addCallback('showCaptions', showCaptions);
      }

      hide(config['state']);

//...Will
      if(ExternalInterface.available)
      {
        ExternalInterface.addCallback('changeCaptionsFile', changeCaptionsFile);
      }
    };

//...Will - toggle captions from JSPAI
    public function toggleCaptions(caps:Boolean):void
    {

//ExternalInterface.call("(function(input){alert(input);})", "Toggle Captions");

      hide(!config['state']);
    };

//...Will - toggle captions with C/c
    public function keyDownHandler(e:KeyboardEvent):void
    {

//ExternalInterface.call("(function(input){alert(input);})", "Toggle Captions");

      // C/c
      if(e.charCode == 67 || e.charCode == 99)
      {
        hide(!config['state']);
      }
    };

//...AM
    public function showCaptions(caps:Boolean):void
    {
//ExternalInterface.call("(function(input){alert('Captions: ' + input);})", caps);
      hide(caps);
    };

    /** Check for captions with a new item. **/
    private function itemHandler(evt:ControllerEvent = null):void
    {
      current         = 0;
      captions        = new Array();
      config['file']  = undefined;
      field.htmlText  = '';
      var file:String;

      if(view.playlist[view.config['item']]['captions.file'])
      {
        file = view.playlist[view.config['item']]['captions.file'];
      }
      else if(view.playlist[view.config['item']]['captions'])
      {
        file = view.playlist[view.config['item']]['captions']; 
      }
      else if(view.config['captions.file'])
      {
        file = view.config['captions.file'];
      }
      else if(view.config['captions'])
      {
        file = view.config['captions'];
      }

      if(file)
      {
        config['file'] = file;

        try
        {
          loader.load(new URLRequest(config['file']));
        }
        catch (err:Error)
        {
          Logger.log(err.message, 'captions');
        }
      }
    };

//...Will - change captions file on-the-fly
    public function changeCaptionsFile(capsfile:String):void
    {
//ExternalInterface.call("(function(input){alert('New Captions File: ' + input);})", capsfile);

      if(capsfile)
      {
        config['file'] = capsfile;

        try
        {
          loader.load(new URLRequest(config['file']));
        }
        catch (err:Error)
        {
          Logger.log(err.message, 'captions');
        }
      }
    };

    /** Captions are loaded; now display them. **/
    private function loaderHandler(evt:Event):void
    {
      var ext:String = config['file'].substr(-3);
//captions = new Array();

      if(ext == 'srt' || ext == 'txt')
      {
        captions = SRTParser.parseCaptions(String(evt.target.data));
      }
      else
      { 
        captions = TTParser.parseCaptions(XML(evt.target.data));
      }

      if(captions.length == 0)
      {
        Logger.log('Not a valid TimedText or SRT file.', 'captions');
      }
    };

    /** Check for captions in metadata. **/
    private function metaHandler(evt:ModelEvent):void
    {
      var txt:String;
      var fnd:Boolean;

      if(evt.data.type == 'caption')
      {
        txt = evt.data.captions;
        fnd = true;
      }
      else if(evt.data.type == 'textdata')
      {
        txt = evt.data.text;
        fnd = true;
      }

      if(fnd == true && !captions)
      {
        field.htmlText = txt + ' ';
        resizeHandler();
        Logger.log(txt, 'caption');
      }
    };

    /** Resize the captions if the display changes. **/
    private function resizeHandler(evt:ControllerEvent = undefined):void
    {
      back.height = field.height + 10;
      width       = view.config['width'];
      scaleY      = scaleX;
      y           = view.config['height'] - height;
    };

    /** Set a caption on screen. **/
    private function setCaption(pos:Number):void
    {
      for(var i:Number = 0; i < captions.length; i++)
      {
        if((captions<em>['begin'] < pos) && ((i == captions.length - 1) || (captions[i + 1]['begin'] > pos)))
        {
          current        = i;
          field.htmlText = captions<em>['text'];
          resizeHandler();
          Logger.log(captions<em>['text'], 'caption');
//...Will
          if(config['listener'])
          {
            try
            {
              ExternalInterface.call(config['listener'], captions<em>['text']);
            }
            catch(err:Error)
            {
            }
          }
          return;
        }
      }
    };

    /** Check state of the player to display captions. **/
    private function stateHandler(evt:ModelEvent):void
    {
      if(((view.config['state'] == ModelStates.PLAYING) || (view.config['state'] == ModelStates.PAUSED)) && config['state'])
      {
        visible = true;
      }
      else
      {
        visible = false;
      }
    };

    /** Check timing of the player to sync captions. **/
    private function timeHandler(evt:ModelEvent):void
    {
      var pos:Number = evt.data.position;

      if(captions && ((captions[current]['begin'] > pos) || (captions[current + 1] && captions[current + 1]['begin'] < pos)))
      {
        setCaption(pos);
      }
    };
  };
}

 
HD files from the playlist has been fixed.

See this thread:

    http://www.longtailvideo.com/support/forum/Plugins/21891/v5-HD-Plugin

nice!exactly what i need ,thanks!!

@Noodle

I have recompiled the captions plugin but I can't make it work. I don't know if is something I have done wrong during compilation (the new size of the plugin is 9.23kb). Is it possible to download the plugin you are using from the developers center?

Thanks.

were you ever able to get this working? i can't

what about the PNG files?

the above code is for audiodescription

what about changing language captions on the fly?

Read it again.

I did pluged-in that addon, it does work, almost. It wont load new files ...

Good work.

Can you find out a way to let your "Change captions on-the-fly" buttons work on different items of the playlist?

 
Keeping with Jeroen's philosophy of, "a clever application of JavaScript...", here is how to display multi-language captions for each track of a playlist, using the existing (modified) plugin.

    <track>
      <jwplayer:author>Devoid of Yesterday</jwplayer:author>
      <jwplayer:title>Things Fall Apart</jwplayer:title>
      <jwplayer:description><![CDATA[This is a terribly long description without a word of truth in it - just nonsense and more nonsense. And here's some HTML formatting, just to make things <font family="arial" size="14px">BIGGER. OF COURSE, THE LARGER FONT EATS UP SPACE FASTER.</font>]]></jwplayer:description>
      <jwplayer:image>http://willswonders.myip.org:8073/video/ThingsFallApart.jpg</jwplayer:image>
      <jwplayer:file>http://willswonders.myip.org:8073/video/ThingsFallApart.flv</jwplayer:file>
      <jwplayer:hd.file>http://willswonders.myip.org:8073/video/ThingsFallApart.mp4</jwplayer:hd.file>
      <jwplayer:provider>http</jwplayer:provider>
      <jwplayer:http.startparam>start</jwplayer:http.startparam>
      <jwplayer:duration>145</jwplayer:duration>
      <strong><jwplayer:captions.file>captions_TFA_english.xml</jwplayer:captions.file></strong>
      <strong><jwplayer:caps1>captions_TFA_english.xml</jwplayer:caps1></strong>
      <strong><jwplayer:caps2>captions_TFA_french.xml</jwplayer:caps2></strong>
      <strong><jwplayer:caps3>captions_TFA_german.xml</jwplayer:caps3></strong>
      <strong><jwplayer:caps4>captions_TFA_italian.xml</jwplayer:caps4></strong>
      <strong><jwplayer:caps5>captions_TFA_japanese.xml</jwplayer:caps5></strong>
      <jwplayer:audiodescription.file>song1.mp3</jwplayer:audiodescription.file>
      <jwplayer:hdsize>600x338</jwplayer:hdsize>
      <jwplayer:sdsize>500x281</jwplayer:sdsize>
    </track>
    <div id="buttons5" class="buttons">
      <button onclick="player.changeCaptionsFile(playlist[player.getConfig()['item']]['caps1']);">Captions ENGLISH</button>
      <button onclick="player.changeCaptionsFile(playlist[player.getConfig()['item']]['caps2']);">Captions FRENCH</button>
      <button onclick="player.changeCaptionsFile(playlist[player.getConfig()['item']]['caps3']);">Captions GERMAN</button>
      <button onclick="player.changeCaptionsFile(playlist[player.getConfig()['item']]['caps4']);">Captions ITALIAN</button>
      <button onclick="player.changeCaptionsFile(playlist[player.getConfig()['item']]['caps5']);">Captions JAPANESE</button>
    </div>

I added the multi-language selection to the first track on the test page at:

    http://willswonders.myip.org:8074/player5/Simple_TitlePlugin_v5.html

The very first caption shows the currently selected language so that you can see that the captions file is
being changed. Sorry, I don't know those other languages — hell, I barely know English — so the captions
are still all in English.

Alf, It's amezing!
That's what we want,

thanks.

 
Now I know what your next question is going to be.

Can the captions be made "sticky"?

In other words, once the user has selected a language, can that apply to all subsequent tracks.

So I did a pre-emptive strike and added sticky captions.

The JS variable stickyCaps and the ITEM Listener are new.

Only tracks 1 & 2 have the sticky captions.

Alf,
You came into my brain :) I love this sticky thing.

Thanks

 
    EXCELLENT!

Post back if you need any help implementing.

mmm interesting, how about refreshing the current captions, without refreshing the whole page/player.

Can u provide a hd plugin for jw player 4.5?

The HD plugin works with V 4.5

Hi, the link http://willswonders.myip.org:8074/player5/Simple_TitlePlugin_v5.html is not working.

Please provide a link to some test page. That will help in understanding the implementation for changing the language of captions.

Thanks.

@Saurabh Mehndiratta - His server is down.

@Ethan - Can you please provide us another test page, since the link is down from a long time.

Thanks :)

Sorry, I can't. This is not our server. I don't have access.