FLV プレイヤーを書いてみた

   _人人人人人人人人人人人人人人人人人人人人_
   >  あんたたち!サンプルよ!サンプルよ!!! <
    ̄^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^ ̄
              _,,,, --──-- ,,,__
            , '´     __     `ヽ、,ヘ
          .くヽ_r'_ヽ 、 ,、_) ヽ ,______r'´イ´
          ['、イ_,-イ、ゝ,_, ,イ_,-,_ゝヽ、__〉, r、
      r 、    ,! 、!-|ーi、λ_L!」/_-i、|〉',ヽイ  / L
      > ヽ  i_ノL.イ (ヒ_]     ヒ_ン ).!_イ  | | /つ  )
     (  と ト、 ヽ! |.i""  ,___,   "" | ! |  |/ "'''ーく ミ
      〈 ⌒  \.| ! ',.   ヽ _ン   .,! ! .|  |     〉
    (⌒ヽ彡ノノ   |  |ヽ、       イノi .|   .|   ミ ミ ̄フ⌒つ
    (と |  彡 | | .| ` ー--─ ´/ /入、  | ミ    / ノ


のっけからごめんなさい。FLV を動的にロードして再生するサンプル一式です。Flex 3 の SDK さえあればコンパイルできますが、ローカルで試す場合は適宜設定が必要です。『信頼されているローカル SWF ファイル』とかで検索して調べて下さい。

まずはファイルの構成から。

  • mainApp.mxml // ビューに当たる部分
  • AppConfig.as // 各種コンフィグ
  • VideoPlayer.as // 各コンポーネントのコントローラに当たる部分
  • mycomp(ディレクトリ)// カスタムコンポーネントを含むディレクト
    • MyHSlider.as // HSldier の拡張
    • Screen.as // FLV を再生するためのスクリーン
    • ScreenEvent.as // カスタムイベントクラス
    • StringUtil.as // String クラスの操作用

では、各ファイルのソースを載せていきます。長いから続きを使います。

mainApp.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:comp="mycomp.*" initialize="init()">
  <mx:Script source="VideoPlayer.as"/>

  <mx:Canvas id="baseCanvas">
    <comp:Screen id="s" x="0" y="0" width="320" height="240"/>
    <mx:Label id="timer" x="0" y="250"/>
    <mx:Button id="playButton" x="0" y="280" toggle="true" label="{AppConfig.pauseLabel}"/>
    <mx:Button id="roopButton" x="250" y="280" toggle="true" label="{AppConfig.offLabel}"/>
    <comp:MyHSlider id="seekBar" x="80" y="280" liveDragging="true" minimum="0" maximum="100" width="120"/>
    <mx:VSlider id="volumeBar" x="220" y="240" liveDragging="true" minimum="0" maximum="1" height="50" value="1"/>
  </mx:Canvas>
</mx:Application>

ここはビューなので、配置等は適宜変更して下さい。

AppConfig.as

package
{
  public class AppConfig {
    public static const defaultLoadPath:String = "test.flv";
    // MyHSlider のスキン設定用
    public static const slideBarImagePath:String = "img/slideBar.png";
    public static const slideThumbImagePath:String = "img/slideBar.png";
    // VideoPlayer の再生ボタン
    public static const playLabel:String = "Play";
    public static const pauseLabel:String = "Pause";
    // 同、ループボタン
    public static const onLabel:String = "Loop On";
    public static const offLabel:String = "Loop Off";
  }
}

見ての通り、コンフィグ値を定数にしているだけです。パッケージも無名で十分。

VideoPlayer.as

import flash.events.*;
import mx.containers.*;
import mx.controls.*;
import mx.core.*;
import mycomp.*;

private var path:String;

private function init():void {
  // フレームごとに実行するイベント
  this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);

  // HTML から FLV のパスを取得
  if(Application.application.parameters.path) {
    path = Application.application.parameters.path;
  }else {
    path = AppConfig.defaultLoadPath;
  }

  // ビデオを標示するスクリーンの初期化
  s.load(path);
  s.addEventListener(ScreenEvent.PLAY_COMPLETE, completeHandler);

  // 再生・一時停止の切り替えボタンの初期化
  playButton.addEventListener(MouseEvent.CLICK, playStatusHandler);

  // ループ再生の切り替えボタンの初期化
  roopButton.addEventListener(MouseEvent.CLICK, roopStatusHandler);

  // シークバーの初期化
  seekBar.addEventListener(Event.CHANGE, seekBarHandler);
  seekBar.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
  seekBar.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);

  // ボリュームコントロールの初期化
  volumeBar.addEventListener(Event.CHANGE, volumeHandler);
}

private function enterFrameHandler(e:Event):void {
  seekBar.drawLoaded(s.getLoadedBytes() / s.getTotalBytes());
  setTimer();

  if(playButton.selected == false) {
    seekBar.value = s.getOffset() / s.getPlayTime() * 100;
  }
}

private function playStatusHandler(e:MouseEvent):void {
  s.togglePause();
  playButton.label = playButton.selected ? AppConfig.playLabel : AppConfig.pauseLabel;
}

// シークバーの位置にあわせてシークする
private function seekBarHandler(e:Event):void {
  // 最大値になったらほんの少し前に戻しておく
  if(seekBar.value >= seekBar.maximum) {
    var val:Number = Math.floor(seekBar.maximum * 0.99);

    seekBar.value = val;
    s.seek(s.getPlayTime() * val);
    // ロードされていない部分になったらロードされた終端に戻しておく
  }else if(seekBar.value >= (seekBar.maximum * s.getLoadedBytes() / s.getTotalBytes())) {
    s.seek(seekBar.maximum * s.getLoadedBytes() / s.getTotalBytes());
  }else {
    s.seek(s.getPlayTime() * seekBar.value / seekBar.maximum);
  }

  setTimer();
}

private function mouseDownHandler(e:MouseEvent):void {
  s.pause();
}

private function mouseUpHandler(e:MouseEvent):void {
  if(playButton.selected == false) {
    s.togglePause();
  }
}

// ボリュームの調節
private function volumeHandler(e:Event):void {
  s.setVolume(volumeBar.value);
}

// 再生終了時
private function completeHandler(e:ScreenEvent):void {
  s.pause();
  s.seek(0);
  seekBar.value = 0;
  setTimer();

  // ループボタンの状況により、ループするかどうか設定
  if(roopButton.selected == true) {
    s.play();
    playButton.selected = false;
    playButton.label = AppConfig.pauseLabel;
  }else {
    playButton.selected = true;
    playButton.label = AppConfig.playLabel;
  }
}

private function roopStatusHandler(e:MouseEvent):void {
  roopButton.label = roopButton.selected ? AppConfig.onLabel : AppConfig.offLabel;
}

// タイマーの値をセットする
private function setTimer():void {
  timer.text = StringUtil.toTime(s.getOffset()) + "/" + StringUtil.toTime(s.getPlayTime());
}

配置されたコントローラにイベントなどを設定していきます。一部カスタムコンポーネントのメソッドを呼び出したりしている部分がありますが、そこは個々のコンポーネントを確認して下さい。

MyHSlider.as

package mycomp
{
  import mx.controls.*;

  public class MyHSlider extends HSlider
  {
    public function MyHSlider() {
      super();
    }

    // どこまで再生が終わったか、見た目で分かるよう表示する
    public function drawLoaded(percentage:Number):void {
      if(percentage < 1) {
        this.graphics.clear();
        this.graphics.beginFill(0xFF0000);
        this.graphics.drawRoundRect(7, 10, this.width * 0.9 * percentage, 3, 1);
      }
    }
  }
}

HSlider を拡張して、どこまでロードが終わったかを表示するバーをつけてみました。Youtube とかニコニコでおなじみのあれです。

Screen.as

package mycomp
{
  import flash.events.*;
  import flash.media.*;
  import flash.net.*;
  import flash.utils.*;

  import mx.core.*;

  public class Screen extends UIComponent
  {
    private var path:String;

    private var con:NetConnection;
    private var stream:NetStream;
    private var video:Video;
    private var playTime:Number;

    public function Screen(){
      super();
    }

    public function load(path:String):void {
      this.path = path;

      con = new NetConnection();
      con.addEventListener(NetStatusEvent.NET_STATUS, netStatusHandler);
      con.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
      con.connect(null);
    }

    private function securityErrorHandler(e:SecurityErrorEvent):void {
      trace("securityErrorHandler: " + e);
    }

    private function netStatusHandler(e:NetStatusEvent):void {
      switch (e.info.code) {
        case "NetConnection.Connect.Success":
          connectStream();
          break;
        case "NetStream.Play.StreamNotFound":
          trace("Unable to locate video: " + this.path);
          break;
      }
    }

    private function connectStream():void {
      stream = new NetStream(con);
      // メタデータの取得用
      stream.client = this;

      // ストリームの状態をイベントで監視
      stream.addEventListener(AsyncErrorEvent.ASYNC_ERROR, asyncErrorHandler);
      stream.addEventListener(NetStatusEvent.NET_STATUS, nsStatusHandler);

      // ビデオを定義し、ストリームをアタッチ
      video = new Video();
      video.attachNetStream(stream);
      stream.play(path);
      addChild(video);
    }

    private function asyncErrorHandler(e:AsyncErrorEvent):void {
      // ignore AsyncErrorEvent events.
    }

    private function nsStatusHandler(e:NetStatusEvent):void {
      switch(e.info.code) {
      case "NetStream.Play.Stop":
        dispatchEvent(new ScreenEvent(ScreenEvent.PLAY_COMPLETE));
        break;
      // もしエンドがとれない flv を使わざるを得ない場合
      // 上の case 節をコメントアウトし
      // ここと checkTime メソッドのコメントを外す
      /*
      case "NetStream.Buffer.Empty":
        checkTime();
        break;
      */
      }
    }

    /*
    private function checkTime():void {
      if(playTime && stream.time >= Math.floor(playTime)) {
        dispatchEvent(new ScreenEvent(ScreenEvent.PLAY_COMPLETE));
      }
    }
    */

    // ビデオのメタデータを取得
    public function onMetaData(param:Object):void {
      playTime = param.duration;
    }

    // 総再生時間を返す
    public function getPlayTime():Number {
      if(playTime) {
        return playTime;
      }else {
        return 0;
      }
    }

    // 現在の再生時間を返す
    public function getOffset():Number {
      if(stream != null) {
        return stream.time;
      }else {
        return 0;
      }
    }

    // ロード済みのバイト数を返す
    public function getLoadedBytes():Number {
      if(stream != null) {
        return stream.bytesLoaded;
      }else {
        return 0;
      }
    }

    // 総バイト数を返す
    public function getTotalBytes():Number {
      if(stream != null) {
        return stream.bytesTotal;
      }else {
        return 1;
      }
    }

    // 再生・一時停止の切り替え
    public function togglePause():void {
      stream.togglePause();
    }

    // 再生
    public function play():void {
      stream.play(path);
    }

    // 一時停止
    public function pause():void {
      stream.pause();
    }

    // 再生位置のシーク
    public function seek(offset:Number):void {
      stream.seek(offset);
    }

    // ボリュームの調整
    public function setVolume(value:Number):void {
      var transform:SoundTransform = new SoundTransform();
      transform.volume = value;
      stream.soundTransform = transform;
    }
  }
}

これが一番の肝になるコンポーネントです。再生終了の判定がうまく取れない FLV もあるとかないとか聞いたので、そうした FLV があるようならコメントアウトした部分を読んで下さい。

ScreenEvent.as

package mycomp
{
  import flash.events.*;

  public class ScreenEvent extends Event
  {
    public static const PLAY_COMPLETE:String = "play_complete";

    public function ScreenEvent(type:String, bubbles:Boolean = false, cancelable:Boolean = false)
    {
      super(type, bubbles, cancelable);
    }

    public override function clone():Event {
      return new ScreenEvent(type, bubbles, cancelable);
    }
  }
}

再生終了時のカスタムイベントです。カスタムイベントを使うときはどこでイベントを発生させて、どこで受け取るのかに注意して下さい。

StringUtil.as

package mycomp
{
  public class StringUtil
  {
    // 桁そろえを行う
    public static function figureFormat(obj:Object, figure:int):String {
      var str:String = obj.toString();

      if(str.length < figure) {
        for(var i:int = str.length; i < figure; i ++) {
          str = "0" + str;
        }
      }

      return str;
    }

    // 与えられた数字を秒数とみなし、時分秒の形式にフォーマットする
    public static function toTime(seconds:Number):String {
      var h:int = seconds / 3600;
      var m:int = (seconds - h * 60) / 60;
      var s:int = seconds % 60;

      return h + ":" + StringUtil.figureFormat(m, 2) + ":" + StringUtil.figureFormat(s, 2);
    }
  }
}

とても簡易的ですが、時刻表示に使うメソッドをまとめたものです。

使い方

html から FLV のパスを渡す形式になっているので、以下を参考に path という変数を渡してみて下さい。

<html>
  <head>
    <title>FLV Player</title>
  </head>
  <body>
    <object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
      id="myflash" width="100%" height="100%"
      codebase="http://fpdownload.macromedia.com/get/flashplayer/current/swflash.cab">
      <param name="movie" value="mainApp.swf" />
      <param name="quality" value="high" />
      <param name="bgcolor" value="#869ca7" />
      <param name="allowScriptAccess" value="sameDomain" />

      <param name="flashvars" value='path=test.flv'>
      <embed src="mainApp.swf?path=test.flv" quality="high" bgcolor="#869ca7"
      flashvars='path=test.flv'
      width="100%" height="100%" name="myflash" align="middle"
      play="true"
      loop="false"
      quality="high"
      allowScriptAccess="sameDomain"
      type="application/x-shockwave-flash"
      pluginspage="http://www.adobe.com/go/getflashplayer">
      </embed>
    </object>
  </body>
</html>

なお、パスが渡されていない場合は AppConfig.as で設定したデフォルトのパス(test.flv)を読みにいきます。swf を直接叩いてテストするなら、同名のファイルを用意して下さい(ニコニコ動画あたりから落とすのが手っ取り早いです)。

大体以上で完了。ここ、頭悪いとか指摘がありましたらご教授頂けると幸いです。これが、サンプルだ!