訳あって、動画再生中の画像をキャプチャしたかったので、簡単な動画再生アプリをC#で作ってみました。
C#で動画再生(メディアプレーヤー)機能を搭載したプログラムを開発する場合、マイクロソフト標準の MediaElement というコントロールを使います。
今回紹介するサンプルプログラムは MediaElement の使い方を紹介することが目的なので、本来メディアプレーヤーが備えておくべき機能や制御は省略しており、かつ System.Windows.Form などの参照設定や、NuGetでインストールが必要なライブラリは使っていません。
それゆえ、ソースをコピーしてお使いのVisual Studio 2022 に張り付けて頂ければ、そのまま動作すると思います。
ちなみに、C#で動画再生を行う方法については多くの方が解説されていますが、本記事では基本的な操作である「再生」、「停止」、「再生位置の変更」だけでなく、他ではあまり触れられていない「再生スピード」、「ボリューム」、「画像キャプチャ」についても解説しています。
また、本記事で紹介するサンプルプログラムについては、Visual Studio 2022 のソリューションファイルとして一式をダウンロードできるようにしています。
是非参考にしてみてください。
サンプルプログラムの画面
本記事では、下記のプログラムを前提に説明を行っています。
MediaElement の設定方法
C# で動画ファイルを再生する場合、マイクロソフト標準の MediaElement というコントロールを使用します。
このコントロールは、MP4、AVI、WMV、MKVなど多くのフォーマットに対応し、4K、8KのVR動画さえ再生することが可能です。
また、任意の位置からの再生、ボリュームや再生スピードの制御など、メディアプレーヤーとして必要な機能が揃っています。
利用するのは簡単で、Visual Studio のレイアウトエディタからコントロールをドラッグ&ドロップするか、XAMLに直接 <MediaElement ~> と記述するだけで使えるようになります。
この時、MediaElementの LoadedBehavior を "Manual" に設定しておかないとプログラムから制御が出来ませんので、XAMLに必ず記述して下さい。
<Window x:Class="MediaPlayer.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MediaPlayer"
mc:Ignorable="d"
Title="MediaPlayer"
Height="460" Width="900">
<Grid >
<MediaElement x:Name="uxVideoPlayer" LoadedBehavior="Manual" Stretch="Uniform" />
</Grid>
<Window>
MediaElement の使い方
詳しくは下記のマイクロソフトの公式ページに記載されている内容を元に分かりやすく説明しています。
基本機能
MediaElement でよく使うメソッドとプロパティは次の通りです。
機能 | 種類 | 名前 | 説明 |
---|---|---|---|
動画ファイルの指定 | プロパティ | Source | 再生したい動画を以下の様に指定します。 Source = new Uri(動画ファイルのパス) |
動画の再生 | メソッド | Start() | 動画を再生します。 |
動画の停止 | メソッド | Stop() | 動画を停止します。Stop()の後にStart()を呼ぶと、 動画の先頭から再生されます。 |
動画の一時停止 | メソッド | Pause() | 一時停止します。Pause()の後にStart()を呼ぶと、 動画の続きを再生します。 |
画面を閉じる (画面をクリアする) | メソッド | Close() | 画面を閉じる=画面の表示をクリアします。Stop()で再生は 停止しますが、画面表示が残ったままになるため Close()で表示をクリアします。 |
再生位置の移動 | プロパティ | Position | 再生位置をミリ秒で指定します。 Position=new TimeSpan(0, 0, 0, 0, 再生位置) 動画の途中に移動したい場合 → Position = new TimeSpan(0, 0, 0, 0, 0) 8分後に指定したい場合(8*60*1000=3600000) →Position = new TimeSpan(0, 0, 0, 0, 3600000) 現在の再生位置を取得する場合、 →Position.ToString() ちなみに、hh:mm:ss.xxxxxx の形式で返ってくるため 秒単位に丸めたい場合は Substring(0,8)を付けます。 →Position.ToString().Substring(0,8) |
音量の設定 | プロパティ | Volume | 音量を0~1までの実数を設定します。(例 0.225412) 0~100 で指定したい場合、Volume から受け取る時は 100を掛け、Volumeに設定する場合は100で割ります。 |
再生速度の設定 | プロパティ | SpeedRatio | 再生速度を0~10までの実数で設定します。(例 1.228123) |
今回は MediaElement に uxVideoPlayer という名前を付けています。
<MediaElement x:Name="uxVideoPlayer" LoadedBehavior="Manual" Stretch="Uniform" />
下記のサンプルは、 uxVideoPlayerに対してプロパティの設定とメソッドの呼び出しを行う具体例です。本格的なメディアプレーヤーアプリを作るには、考慮すべき点が多々ありますが、非常に簡単な操作で動画再生を制御できることが、お分かりいただけたかと思います。
//音量の設定
uxVideoPlayer.Volume = 0.5;
//再生速度の設定
uxVideoPlayer.SpeedRatio = 1.0;
//動画ファイルの指定
uxVideoPlayer.Source = new Uri(@"D:\Movies\StarGateVol1.MP4");
//動画の再生
uxVideoPlayer.Play();
//再生位置の移動(0時8分から再生)
uxVideoPlayer.Position = new TimeSpan(0, 0, 0, 0, 8*60*1000);
//一時停止
uxVideoPlayer.Pause();
//停止
uxVideoPlayer.Stop();
動画のキャプチャについて
再生中の画面をファイルに保存するのは少々煩雑なので、下記の通り関数化しました。このままコピペしてお使いください。
/// <summary>
/// 動画のキャプチャを取る
/// </summary>
/// <param name="mediaElement"></param>
/// <param name="fileName"></param>
private void SaveFrame(MediaElement mediaElement, string fileName)
{
//動画が再生されていないと例外が発生するので、try~catchで例外を無効化
try
{
// キャプチャ対象のフレームをRenderTargetBitmapにレンダリングする
RenderTargetBitmap renderTargetBitmap = new RenderTargetBitmap(
(int)mediaElement.ActualWidth,
(int)mediaElement.ActualHeight,
96, 96, PixelFormats.Pbgra32);
renderTargetBitmap.Render(mediaElement);
// ファイルに保存する
using (FileStream fileStream = new FileStream(fileName, FileMode.Create))
{
JpegBitmapEncoder encoder = new JpegBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(renderTargetBitmap));
encoder.Save(fileStream);
}
}
catch { }
}
応用例(簡易メディアプレーヤー)
今回作成したサンプルプログラムの画面を説明しておきます。
再生位置、ボリューム、再生速度はスライダーでの指定が可能で、再生に合わせた時刻表示やスライダーバーの位置更新についもて、タイマー割込みを使って実現しました。
また、画面キャプチャの出力先も、MyPicture フォルダ固定になっています。フォルダ選択ダイアログを使う場合、System.Window.Forms の参照設定が必要になるため、これも省略しています。
この辺は、必要に応じて修正して頂ければと思います。
プロジェクト一式のダウンロード
Visual Studio 2022でビルドできるソリューションファイル一式は以下からダウンロードできます。
簡易メディアプレーヤーVisualStudio2022用ソリューション一式
ソースコード
まず、XAMLのソースコードです。
<Window x:Class="MediaPlayer.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MediaPlayer"
mc:Ignorable="d"
Title="MediaPlayer"
Height="460" Width="900">
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="409*"/>
<RowDefinition Height="30"/>
</Grid.RowDefinitions>
<MediaElement x:Name="uxVideoPlayer" Grid.Row="0" LoadedBehavior="Manual" UnloadedBehavior="Stop" Stretch="Uniform" MediaOpened="uxVideoPlayer_MediaOpened"/>
<StackPanel Orientation="Horizontal" Grid.Row="1">
<Button Content="📁" Click="OpenFileButton_Click" Margin="5" Background="{x:Null}" BorderThickness="0" />
<Button Content="▶" Click="Start_Click" Margin="5" Background="{x:Null}" BorderThickness="0" />
<Button Content="■" Click="Stop_Click" Margin="5" Background="{x:Null}" BorderThickness="0"/>
<Button Content="⏸" Click="Pause_Click" Margin="5" Background="{x:Null}" BorderThickness="0"/>
<Slider x:Name="uxTimeSlider" Value="0" Width="300" Height="20" Margin="5" ValueChanged="uxTimeSlider_ValueChanged" PreviewMouseDown="uxTimeSlider_PreviewMouseDown" PreviewMouseUp="uxTimeSlider_PreviewMouseUp"></Slider>
<TextBlock x:Name="uxTime" Width="50" VerticalAlignment="Center" HorizontalAlignment="Center"></TextBlock>
<Slider x:Name="uxVolumeSlider" Minimum="0" Maximum="1" Value="0.5" Width="100" Height="20" Margin="5" ValueChanged="uxVolumeSlider_ValueChanged" ></Slider>
<TextBlock x:Name="uxVolume" Width="40" VerticalAlignment="Center" HorizontalAlignment="Center"></TextBlock>
<Slider x:Name="uxSpeedSlider" Value="1" Width="80" Height="20" Margin="5" ValueChanged="uxSpeedSlider_ValueChanged" ></Slider>
<TextBlock x:Name="uxSpeed" Width="40" VerticalAlignment="Center" HorizontalAlignment="Center" ></TextBlock>
<Button Content="📷" Click="Capture_Click" Margin="5" Background="{x:Null}" BorderThickness="0" />
</StackPanel>
</Grid>
</Window>
次に、C#側のソースコードです。
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
namespace MediaPlayer
{
/// <summary>
/// MainWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainWindow : Window
{
// 再生時刻を表示するためのタイマー
private DispatcherTimer _timer = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(1) };
// タイムスライダーのドラッグ中を判定するフラグ
private bool _isSliderDragging = false;
public MainWindow()
{
InitializeComponent();
// 再生時刻表示用タイマーにイベントハンドラを登録
_timer.Tick += Timer_Tick;
// 各種コントロールのリセット
ResetVideoPlayer();
}
/// <summary>
/// ビデオ再生関連のコントロールの値をリセット
/// </summary>
public void ResetVideoPlayer()
{
//ボリュームと再生速度のスライダー初期値をリセット
uxTimeSlider.Value = 0;
uxTime.Text = "00:00:00";
uxVolume.Text = ((int)(uxVolumeSlider.Value * 100)).ToString();
uxSpeed.Text = Math.Round(uxSpeedSlider.Value, 1).ToString();
// MediaElement の値をリセット
uxVideoPlayer.Position = TimeSpan.FromMilliseconds(uxTimeSlider.Value);
uxVideoPlayer.Volume = (double)uxVolumeSlider.Value;
uxVideoPlayer.SpeedRatio = (double)uxSpeedSlider.Value;
}
/// <summary>
/// 再生時刻の表示
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Timer_Tick(object sender, EventArgs e)
{
if (uxVideoPlayer.NaturalDuration.HasTimeSpan)
{
if (! _isSliderDragging)
{
uxTimeSlider.Value = uxVideoPlayer.Position.TotalMilliseconds;
uxTime.Text = uxVideoPlayer.Position.ToString().Substring(0, 8);
}
}
}
/// <summary>
/// 再生ボタンクリック時のイベントハンドラ
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Start_Click(object sender, RoutedEventArgs e)
{
uxVideoPlayer.Play();
_timer.Start();
}
/// <summary>
/// 一時停止ボタンクリック時のイベントハンドラ
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Pause_Click(object sender, RoutedEventArgs e)
{
uxVideoPlayer.Pause();
_timer.Stop();
}
/// <summary>
/// ストップボタンクリック時のイベントハンドラ
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Stop_Click(object sender, RoutedEventArgs e)
{
uxVideoPlayer.Position = TimeSpan.FromMilliseconds(0);
uxVideoPlayer.Stop();
uxVideoPlayer.Close();
_timer.Stop();
ResetVideoPlayer();
}
/// <summary>
/// 動画ファイルをオープンした時のイベントハンドラ
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OpenFileButton_Click(object sender, RoutedEventArgs e)
{
var dialog = new OpenFileDialog();
dialog.Filter = "Video Files|*.mp4;*.mkv;*.avi;*.wmv|All Files|*.*";
if (dialog.ShowDialog() == true)
{
// 選択された動画ファイルを再生
uxVideoPlayer.Source = new Uri(dialog.FileName);
uxVideoPlayer.Play();
_timer.Start();
}
}
/// <summary>
/// タイムラインスライダーの値が変化した時のイベントハンドラ
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void uxTimeSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
// タイムラインスライダーがドラッグ中の時はスライダーの値で時刻表示を更新
if (_isSliderDragging)
{
uxTime.Text = TimeSpan.FromMilliseconds(uxTimeSlider.Value).ToString().Substring(0, 8);
}
}
/// <summary>
/// タイムラインスライダーのマウスが押された時(ドラッグ開始)のイベントハンドラ
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void uxTimeSlider_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
_isSliderDragging = true;
}
/// <summary>
/// タイムラインスライダーのマウスが離された時(ドラッグ終了)のイベントハンドラ
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void uxTimeSlider_PreviewMouseUp(object sender, MouseButtonEventArgs e)
{
_isSliderDragging = false;
uxVideoPlayer.Position = TimeSpan.FromMilliseconds(uxTimeSlider.Value);
}
/// <summary>
/// 動画ファイル再生開始のイベントハンドラ
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void uxVideoPlayer_MediaOpened(object sender, RoutedEventArgs e)
{
uxTimeSlider.Maximum = uxVideoPlayer.NaturalDuration.TimeSpan.TotalMilliseconds;
}
/// <summary>
/// ボリュームスライダーのイベントハンドラ
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void uxVolumeSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
uxVideoPlayer.Volume = (double)uxVolumeSlider.Value;
if (uxVolume != null)
{
uxVolume.Text = ((int)(uxVideoPlayer.Volume * 100)).ToString();
}
}
/// <summary>
/// 再生スピードスライダーのイベントハンドラ
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void uxSpeedSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
uxVideoPlayer.SpeedRatio = (double)uxSpeedSlider.Value;
if (uxSpeed != null)
{
uxSpeed.Text = Math.Round(uxVideoPlayer.SpeedRatio,1).ToString();
}
}
/// <summary>
/// キャプチャボタンのイベントハンドラ
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Capture_Click(object sender, RoutedEventArgs e)
{
var folder = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
var filename = "Capture_" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".jpg";
SaveFrame(uxVideoPlayer, System.IO.Path.Combine(folder, filename));
}
/// <summary>
/// 動画のキャプチャを取る
/// </summary>
/// <param name="mediaElement"></param>
/// <param name="fileName"></param>
private void SaveFrame(MediaElement mediaElement, string fileName)
{
//動画が再生されていないと例外が発生するので、try~catchで例外を無効化
try
{
// キャプチャ対象のフレームをRenderTargetBitmapにレンダリングする
RenderTargetBitmap renderTargetBitmap = new RenderTargetBitmap(
(int)mediaElement.ActualWidth,
(int)mediaElement.ActualHeight,
96, 96, PixelFormats.Pbgra32);
renderTargetBitmap.Render(mediaElement);
// ファイルに保存する
using (FileStream fileStream = new FileStream(fileName, FileMode.Create))
{
JpegBitmapEncoder encoder = new JpegBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(renderTargetBitmap));
encoder.Save(fileStream);
}
}
catch { }
}
}
}
補足説明
全体的にそれほど難しくはないと思いますが、スライダーによる再生位置の指定と、再生中の再生時刻表示について簡単に細くしておきます。
スライダーによる再生位置の指定
再生位置のスライダーを動かすとValueChangedイベントが発生するので、そこで Position プロパティにスライダーの Value を設定してあげれば、スライダーの位置に合わせた動画の再生が可能になります。
しかし、実際にはスライダーを少し動かすだけで数十回のイベントが発生してしまうため、MediaElement の再生が後追いになってしまいます。その結果、スライダーの操作を止めてから秒間コマ送り状態が続き、ようやく指定位置からの再生が開始されます。
本来は、スライダーを動かしている間は再生時刻だけが変化し、スライダーの操作を止めた時点でその再生時刻から再生するという挙動が望ましいので、その実現方法として以下の処理を行いました。
- スライダーのMouseDown と MouseUpイベントを使って、スライダー操作中(ドラッグ中)か否かを判断できるようにする(_isSliderDraggingによるフラグ制御)
- ValueChanged発生時、スライダー操作中の時は、スライダーの位置に合わせて再生時刻を更新する。
- スライダーの MouseUpイベントでスライダー操作が完了したと判断し、そこでスライダーのValueを Position プロパティにセット(スライダーに合わせて再生位置を変更)する。
/// タイムラインスライダーの値が変化した時のイベントハンドラ
private void uxTimeSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
// タイムラインスライダーがドラッグ中の時はスライダーの値で時刻表示を更新
if (_isSliderDragging)
{
uxTime.Text = TimeSpan.FromMilliseconds(uxTimeSlider.Value).ToString().Substring(0, 8);
}
}
/// タイムラインスライダーのマウスが離された時(ドラッグ終了)のイベントハンドラ
private void uxTimeSlider_PreviewMouseUp(object sender, MouseButtonEventArgs e)
{
_isSliderDragging = false;
uxVideoPlayer.Position = TimeSpan.FromMilliseconds(uxTimeSlider.Value);
}
/// タイムラインスライダーのマウスが離された時(ドラッグ終了)のイベントハンドラ
private void uxTimeSlider_PreviewMouseUp(object sender, MouseButtonEventArgs e)
{
_isSliderDragging = false;
uxVideoPlayer.Position = TimeSpan.FromMilliseconds(uxTimeSlider.Value);
}
再生中の再生時刻表示
再生中の時刻表示はタイマー割込みを使いました。1秒ごとにタイマー割込みを発生させ、その中で MediaElementの Position メソッドから再生時刻を取得、タイムラインスライダーのValue に値をセットしています。
この時、スライダー操作中はタイマーによる再生時刻の表示が行なわれないよう、_isSliderDragging を使って制御しています。
// 再生時刻を表示するためのタイマー
private DispatcherTimer _timer = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(1) };
public MainWindow()
{
InitializeComponent();
// 再生時刻表示用タイマーにイベントハンドラを登録
_timer.Tick += Timer_Tick;
}
/// 再生時刻の表示
private void Timer_Tick(object sender, EventArgs e)
{
if (uxVideoPlayer.NaturalDuration.HasTimeSpan)
{
if (! _isSliderDragging)
{
uxTimeSlider.Value = uxVideoPlayer.Position.TotalMilliseconds;
uxTime.Text = uxVideoPlayer.Position.ToString().Substring(0, 8);
}
}
}
/// 再生ボタンクリック時のイベントハンドラ
private void Start_Click(object sender, RoutedEventArgs e)
{
uxVideoPlayer.Play();
_timer.Start();
}
/// 一時停止ボタンクリック時のイベントハンドラ
private void Pause_Click(object sender, RoutedEventArgs e)
{
uxVideoPlayer.Pause();
_timer.Stop();
}
まとめ
今回はC#とWPFによるWindowsアプリにおいて、MediaElement コントロールを使った動画再生の方法と、再生中の画像のキャプチャについて解説しました。
MediaElement はマイクロソフト標準のコントロールであり、様々な動画フォーマットに対応しており、8K動画やVR動画もちゃんと再生してくれる優れものです。
動画の再生、停止、一時停止の他、音量、再生スピード、再生位置の指定など、メディアプレーヤーのアプリを自作するために必要な機能が揃っています。
本格的なメディアプレーヤーを作成するには、ドラッグ&ドロップによるファイル指定やプレイリストの編集機能など、様々な機能が必要になりますが、簡易的に動画をちょこっと再生させたい場合などは重宝します。
今回紹介したサンプルプログラムは中途半端になってはいますが、必要な機能を盛り込みやすくなっていると思いますので、是非自分用のオリジナルメディアプレーヤー向けに改造して、役立てていただければと思います。
コメント