前回はJavaFXの基本構造について確認した。今回はいよいよ当初の目的であった動画再生を試してみる。
■ 動画再生の仕組み
JavaFXでの動画や音声の再生はメディアエンジンが自動実行する。この際の基本構造は以下の図のようになる。
クラス |
役割 |
メディア
(Media) |
再生するメディア・リソース情報を保持するクラス |
メディア・プレイヤー
(MediaPlayer) |
メディアの再生・停止などの操作を実施するクラス
メディアクラスのインスタンスを保持する |
メディア・ビュー
(MediaView) |
動画などのメディア表示するため、シーングラフへ登録するクラス
メディア・プライヤークラスのインスタンスを保持する |
■ 対応フォーマット(詳細)
JavaFXが対応しているメディアフォーマットは、以下の組み合わせのとおり。ただし、デコード・エンジンはOSで設定されているものを利用する。このため、デコーダがない場合は再生ができない。
拡張子 |
ビデオ・フォーマット |
音声フォーマット |
FLV |
VP6 |
MP3 |
MPEG-4 |
H.264/AVC |
AAC |
MP3 |
- |
MP3 |
AIFF |
- |
AIFF(非圧縮PCM) |
WAV |
- |
WAV(非圧縮PCM) |
■ サンプル
以下では、相対パス『movie/battle_without.mp4』にあるメディアファイル(音声付の動画)をとりあえず再生するサンプルプログラムを示す。ちなみに、手元に再生可能な動画ファイルがない場合、Oracleのサンプル動画『
http://download.oracle.com/otndocs/products/javafx/oow2010-2.flv』であれば確実に再生できるはずである。
実行結果
フォルダ構成
TestJavaFX
┣src
┃┗application
┃ ┗TestFlvPlay.java
┗movie
┗battle_without.mp4
ソースコード
package application;
import java.io.File;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaView;
import javafx.stage.Stage;
public class TestFlvPlay extends Application {
@Override
public void start(Stage primaryStage) {
try {
// シーングラフを作成
StackPane Pane = new StackPane();
// 動画ファイルのパスを取得
File f = new File( "movie\\battle_without.mp4" );
// 動画再生クラスをインスタンス化
Media Video = new Media( f.toURI().toString() );
MediaPlayer Play = new MediaPlayer(Video);
MediaView mediaView = new MediaView(Play);
// シーングラフに追加
Pane.getChildren().add(mediaView);
// シーンを追加
Scene scene = new Scene(Pane, 750, 500);
// ステージ設定
primaryStage.setTitle("VideoPlay");
primaryStage.setScene(scene);
primaryStage.show();
Play.play();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
上記のサンプルのポイントを説明すると…
- 25行目では、再生するメディアファイルの絶対パス(URI)文字列をMediaクラスに渡している。(ここでは相対パス『movie/battle_without.mp4』が絶対パスに展開されている)
- 26行目では、MediaPlayerクラスにMediaクラスのインスタンスを渡し、再生可能状態としている。
- 27行目で用意したMediaViewクラスのインスタンスは、30行目でシーングラフに登録している
- 37行目ではシーングラフの描画を開始
- 38行目ではメディアファイル(動画と音声)の再生が開始される
■ 動画再生の制御(再生・一時停止・停止・繰返・シーク・ボリューム)
続いて、メディア再生の制御を見ていく。メディア再生の状態は以下の6つが定義されている。
注意が必要なのはPLAYINGとSTOPPEDの違いである。再生時間いっぱいまで再生して、再生が終了した場合でも状態としてはPLAYING状態である。STOPPED状態ヘはstop関数を利用して、明示的に遷移する必要がある。
状態 |
内容 |
UNKNOWN |
プリロード状態。インスタンス化直後など、再生の準備ができていない状態 |
READY |
レディ状態。メディアの再生準備が終了した状態 |
PAUSED |
一時停止状態 |
PLAYING |
再生状態 |
STALLED |
ストール(行詰)状態。データ流入が低速化または停止し、再生を続行するために十分なデータがない状態 |
STOPPED |
停止状態 |
また、主要なメディア制御とプログラミング方法については以下の通りである。
メディア制御 |
プログラミング方法 |
再生 |
再生には、MediaPlayer.play関数を利用する |
一時停止 |
一時停止には、MediaPlayer.pause関数を利用する |
停止 |
停止には、MediaPlayer.stop関数を利用する |
シーク |
再生位置(時間)を変更するには、MediaPlayer.seek関数を利用する |
ボリューム操作 |
音量を変更するには、MediaPlayer.volumeプロパティの値を変更する。範囲は[0.0, 1.0] |
ピッチ変更 |
再生速度を変更するには、MediaPlayer.rateプロパティの値を変更する。範囲は[0.0, 8.0] |
拡大・縮小 |
動画サイズを変更するには、MediaViewのsetFitWidth関数/setFitHeight関数を利用する |
以下に、メディア制御のサンプルコードを示す。サンプルは、再生・一時停止・停止・連続再生・シーク・ボリューム変更が可能な簡易プレイヤである。
◇サンプルコード
package application_fx;
import java.io.File;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.control.ToggleButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaView;
import javafx.stage.Stage;
import javafx.util.Duration;
public class TestMedia extends Application {
public static void main(String[] args)
{
launch( args );
}
@Override
public void start(Stage primaryStage) throws Exception
{
try {
// シーングラフを作成
BorderPane root = new BorderPane();
// 動画ファイルのパスを取得
File f = new File( "movie\\battle_without.mp4" );
// 動画再生クラスをインスタンス化
Media Video = new Media( f.toURI().toString() );
MediaPlayer Play = new MediaPlayer(Video);
MediaView mediaView = new MediaView(Play);
mediaView.setFitWidth( 700 );
root.setCenter( mediaView );
// 画面下に表示する情報バーを作成
HBox bottomNode = new HBox( 10.0 );
bottomNode.getChildren().add( createButton(Play) ); // 再生・停止・繰り返しボタン作成
bottomNode.getChildren().add( createTimeSlider( Play ) ); // 時間表示スライダ作成
bottomNode.getChildren().add( createVolumeSlider( Play ) ); // ボリューム表示スライダ作成
root.setBottom( bottomNode );
// シーンを追加
Scene scene = new Scene( root , 700 , 500 );
// ステージ設定
primaryStage.setTitle( "VideoPlay" );
primaryStage.setScene( scene );
primaryStage.show();
// 動画再生開始
Play.play();
// 動作確認用の出力を設定
Play.currentTimeProperty().addListener( (ov) -> System.out.println( Play.getCurrentTime() ) );
Play.statusProperty().addListener( (ov) -> System.out.println( Play.getStatus() ) );
} catch(Exception e) {
e.printStackTrace();
}
}
/**
* 再生、一時停止、停止、連続再生ボタンを作成
* @param mp
* @return
*/
public Node createButton( MediaPlayer mp )
{
// 表示コンポーネントを作成
HBox root = new HBox( 1.0 );
Button playButton = new Button( "Play" );
Button pauseButton = new Button( "Pause" );
Button stopButton = new Button( "Stop" );
ToggleButton repeatButton = new ToggleButton( "Repeat" );
root.getChildren().add( playButton );
root.getChildren().add( pauseButton );
root.getChildren().add( stopButton );
root.getChildren().add( repeatButton );
// 再生ボタンにイベントを登録
EventHandler<ActionEvent> playHandler = ( e ) ->
{
// 再生開始
mp.play();
};
playButton.addEventHandler( ActionEvent.ACTION , playHandler );
// 一時停止ボタンにイベントを登録
EventHandler<ActionEvent> pauseHandler = ( e ) ->
{
// 一時停止
mp.pause();
};
pauseButton.addEventHandler( ActionEvent.ACTION , pauseHandler );
// 停止ボタンにイベントを登録
EventHandler<ActionEvent> stopHandler = ( e ) ->
{
// 停止
mp.stop();
};
stopButton.addEventHandler( ActionEvent.ACTION , stopHandler );
// 連続再生設定
Runnable repeatFunc = () ->
{
// 連続再生ボタンの状態を取得し
if( repeatButton.isSelected() )
{
// 頭だしして再生
mp.seek( mp.getStartTime() );
mp.play();
}else{
// 頭だしして停止
mp.seek( mp.getStartTime() );
mp.stop();
};
};
mp.setOnEndOfMedia( repeatFunc );
return root;
}
/**
* 再生時間を表示・操作するスライダを作成
* @param mp
* @return
*/
public Node createTimeSlider( MediaPlayer mp )
{
// 表示コンポーネントを作成
HBox root = new HBox( 5.0 );
Slider slider = new Slider();
Label info = new Label();
root.getChildren().add( slider );
root.getChildren().add( info );
// 再生準備完了時に各種情報を取得する関数を登録
Runnable beforeFunc = mp.getOnReady(); // 現在のレディ関数
Runnable readyFunc = () ->
{
// 先に登録された関数を実行
if( beforeFunc != null ){ beforeFunc.run(); }
// スライダの値を設定
slider.setMin( mp.getStartTime().toSeconds() );
slider.setMax( mp.getStopTime().toSeconds() );
slider.setSnapToTicks( true );
};
mp.setOnReady( readyFunc );
// 再生中にスライダを移動
// プレイヤの現在時間が変更されるたびに呼び出されるリスナを登録
ChangeListener<? super Duration> playListener = ( ov , old , current ) ->
{
System.out.println( "here" );
// 動画の情報をラベル出力
String infoStr = String.format( "%4.2f" , mp.getCurrentTime().toSeconds() )
+ "/"
+ String.format( "%4.2f" , mp.getTotalDuration().toSeconds() ) ;
info.setText( infoStr );
// スライダを移動
slider.setValue( mp.getCurrentTime().toSeconds() );
};
mp.currentTimeProperty().addListener( playListener );
// スライダを操作するとシークする
EventHandler<MouseEvent> sliderHandler = ( e ) ->
{
// スライダを操作すると、シークする
mp.seek( javafx.util.Duration.seconds( slider.getValue() ) );
};
slider.addEventFilter( MouseEvent.MOUSE_RELEASED , sliderHandler );
return root;
}
/**
* ボリュームを表示・操作するスライダを作成
* @param mp
* @return
*/
public Node createVolumeSlider( MediaPlayer mp )
{
// 表示コンポーネントを作成
HBox root = new HBox( 5.0 );
Label info = new Label();
Slider slider = new Slider();
root.getChildren().add( info );
root.getChildren().add( slider );
// 再生準備完了時に各種情報を取得する関数を登録
Runnable beforeFunc = mp.getOnReady(); // 現在のレディ関数
Runnable readyFunc = () ->
{
// 先に登録された関数を実行
if( beforeFunc != null ){ beforeFunc.run(); }
// スライダの値を設定
slider.setMin( 0.0 );
slider.setMax( 1.0 );
slider.setValue( mp.getVolume() );
};
mp.setOnReady( readyFunc );
// 再生中にボリュームを表示
// プレイヤの現在時間が変更されるたびに呼び出されるリスナを登録
ChangeListener<? super Number> sliderListener = ( ov , old , current ) ->
{
// 動画の情報をラベル出力
String infoStr = String.format( "Vol:%4.2f" , mp.getVolume() );
info.setText( infoStr );
// スライダにあわせてボリュームを変更
mp.setVolume( slider.getValue() );
};
slider.valueProperty().addListener( sliderListener );
return root;
}
}
◇実行結果
◇解説
メディアファイルの読込~ウィンドウ表示の流れは、前回のサンプルと同様である。追加したロジックについて、説明を行う。なお、サンプルコードではラムダ式やバインディングを多用している。ラムダ式についての説明は『
Java 8の新機能(ラムダ式とストリームAPI)』を、バインディングについての説明は『
JavaFX バインディング』を参照のこと。
- 動画のサイズ変更は47行目で行っている
- ボタン(Play,Pause,Stop,Repeat)の追加は、createButton関数で行っている(52行目、84行目~139行目)。再生、一時停止、停止はそれぞれplay、pause、stop関数を呼び出すだけでよい(98行目~119行目)。連続再生では、再生終了を検知するsetOnEndOfMedia関数にイベントハンドラを追加することで実現する(122行目~136行目)。注意点としては、必ず頭だしをする必要があること。
- シーク・スライダの追加は、createTimeSlider関数で行っている(53行目、147行目~197行目)。スライダの設定はOnReady関数にイベントハンドラを登録して行う(157行目~168行目)。これは、READY状態にならないと、メディアの再生時間などの情報が取得できないためである。
- シーク・スライダのバー自動移動は、再生位置を表すMediaPlayer.currentTimePropertyプロパティの値に対してバインディングを設定している(172行目~184行目)。これによりプロパティ値が変更するたびに、スライダバーの位置と再生時間の更新が行われる。
- シーク・スライダのシークは、スライダ上のマウスリリースを検知するイベントハンドラ内でseek関数を呼び出している(188行目~194行目)。
- ボリューム・スライダの追加は、createVolumeSlider関数で行っている(54行目、204行目~242行目)。シーク・スライダと同様、スライダの設定はonReadyのイベントハンドラで実行している。
■ 参照リンク
- JavaFX: Getting Started with JavaFX
- JavaFX: Incorporating Media Assets Into JavaFX Applications
- JavaDoc - クラスMediaPlayer
- JavaFX: JavaFXアプリケーションへのメディア・アセットの組込み
改訂履歴・2015年10月11日 一部改訂。『動画再生の制御』を追記