2 using System.Collections.Concurrent;
3 using System.Collections.Generic;
4 using System.Diagnostics;
6 using System.Threading;
7 using System.Threading.Tasks;
12 /// 動画のソースリーダーを持ち、タスク(ワーカスレッド)を使ってサンプルをデコードし、キューに蓄えるて提供する。
14 public class 動画デコーダ : FDK.同期.RWLockAction, IDisposable
17 public Func<double> 現在の再生時刻100ns = null;
19 public SharpDX.Size2 サイズdpx
23 SharpDX.Size2 size = SharpDX.Size2.Zero;
24 this.ReadLock( () => {
25 size = this.bs_サイズdpx;
31 this.WriteLock( () => {
32 this.bs_サイズdpx = value;
36 public double ループした際の先頭時刻sec
41 this.ReadLock( () => {
42 sec = this.bs_ループした際の先頭時刻sec;
48 this.WriteLock( () => {
49 this.bs_ループした際の先頭時刻sec = value;
54 public 動画デコーダ( デバイスリソース dr, string 動画ファイルパス, bool ループ再生する, int キューのサイズ )
57 this.キューのサイズ = キューのサイズ;
59 this.ループ再生する = ループ再生する;
60 string 変数付きファイルパス = フォルダ.絶対パスに含まれるフォルダ変数を展開して返す( 動画ファイルパス ); // Log出力用。
62 #region " 動画ファイルパスの有効性を確認する。"
64 if( string.IsNullOrEmpty( 動画ファイルパス ) )
66 Log.ERROR( $"動画ファイルパスが null または空文字列です。[{変数付きファイルパス}]" );
69 if( false == System.IO.File.Exists( 動画ファイルパス ) )
71 Log.ERROR( $"動画ファイルが存在しません。[{変数付きファイルパス}]" );
76 #region " 動画のファイルパス(URI扱い)から SourceReaderEx を作成する。"
80 using( var 属性 = new SharpDX.MediaFoundation.MediaAttributes() )
82 // DXVAに対応しているGPUの場合にはそれをデコードに利用するよう指定する。
83 属性.Set<SharpDX.ComObject>( SharpDX.MediaFoundation.SourceReaderAttributeKeys.D3DManager, dr.DXGIDeviceManager );
85 // 追加のビデオプロセッシングを有効にする。
86 属性.Set<bool>( SharpDX.MediaFoundation.SourceReaderAttributeKeys.EnableAdvancedVideoProcessing, true ); // bool だったり
88 // 追加のビデオプロセッシングを有効にしたら、こちらは無効に。
89 属性.Set<int>( SharpDX.MediaFoundation.SinkWriterAttributeKeys.ReadwriteDisableConverters, 0 ); // int だったり
91 // 属性を使って、SourceReaderEx を生成。
92 using( var sourceReader = new SharpDX.MediaFoundation.SourceReader( 動画ファイルパス, 属性 ) )
94 this.SourceReaderEx = sourceReader.QueryInterface<SharpDX.MediaFoundation.SourceReaderEx>();
98 catch( SharpDX.SharpDXException e )
100 Log.ERROR( $"SourceReaderEx の作成に失敗しました。(0x{e.HResult:x8})[{変数付きファイルパス}]" );
105 #region " 最初のビデオストリームを選択し、その他のすべてのストリームを非選択にする。"
109 this.SourceReaderEx.SetStreamSelection( SharpDX.MediaFoundation.SourceReaderIndex.AllStreams, false );
111 catch( SharpDX.SharpDXException e )
113 Log.ERROR( $"すべてのストリームの選択解除に失敗しました。(0x{e.HResult:x8})[{変数付きファイルパス}]" );
118 this.SourceReaderEx.SetStreamSelection( SharpDX.MediaFoundation.SourceReaderIndex.FirstVideoStream, true );
120 catch( SharpDX.SharpDXException e )
122 Log.ERROR( $"最初のビデオストリームの選択に失敗しました。(0x{e.HResult:x8})[{変数付きファイルパス}]" );
127 #region " ARGB32 フォーマットに合わせたメディアタイプを作成し、SourceReaderEx に登録してデコーダを準備する。"
131 using( var mediaType = new SharpDX.MediaFoundation.MediaType() )
133 // フォーマットを部分メディアタイプの属性に設定。
134 mediaType.Set<Guid>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.MajorType, SharpDX.MediaFoundation.MediaTypeGuids.Video );
135 mediaType.Set<Guid>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.Subtype, SharpDX.MediaFoundation.VideoFormatGuids.Argb32 ); // ARGB32 フォーマットで固定。SourceReaderEx を使わない場合、H264 では NV12 しか選べないので注意。
137 // 部分メディアタイプを SourceReaderEx にセットする。SourceReaderEx は、必要なデコーダをロードするだろう。
138 this.SourceReaderEx.SetCurrentMediaType( SharpDX.MediaFoundation.SourceReaderIndex.FirstVideoStream, mediaType );
141 catch( SharpDX.SharpDXException e )
143 Log.ERROR( $"MediaType (Video, ARGB32) の設定または必要なデコーダの読み込みに失敗しました。(0x{e.HResult:x8})[{変数付きファイルパス}]" );
148 #region " ビデオストリームが選択されていることを再度保証する。"
152 this.SourceReaderEx.SetStreamSelection( SharpDX.MediaFoundation.SourceReaderIndex.FirstVideoStream, true );
154 catch( SharpDX.SharpDXException e )
156 Log.ERROR( $"最初のビデオストリームの選択に失敗しました(MediaType 設定後)。(0x{e.HResult:x8})[{変数付きファイルパス}]" );
161 #region " デコーダの読み込みにより完成した完全メディアタイプを取得する。"
165 this.MediaType = this.SourceReaderEx.GetCurrentMediaType( SharpDX.MediaFoundation.SourceReaderIndex.FirstVideoStream );
167 catch( SharpDX.SharpDXException e )
169 Log.ERROR( $"完全メディアタイプの取得に失敗しました。(0x{e.HResult:x8})[{変数付きファイルパス}]" );
174 #region " フレームサイズを取得する。動画の途中でのサイズ変更には対応しない。"
178 var packedFrameSize = this.MediaType.Get<long>( SharpDX.MediaFoundation.MediaTypeAttributeKeys.FrameSize );
179 this.サイズdpx = new SharpDX.Size2( (int) ( ( packedFrameSize >> 32 ) & 0xFFFFFFFF ), (int) ( ( packedFrameSize ) & 0xFFFFFFFF ) );
181 catch( SharpDX.SharpDXException e )
183 Log.ERROR( $"フレームサイズの取得に失敗しました。(0x{e.HResult:x8})[{変数付きファイルパス}]" );
188 #region " 描画先WICビットマップを作成する。"
192 this.WICビットマップ = new SharpDX.WIC.Bitmap(
193 dr.WicImagingFactory2,
196 SharpDX.WIC.PixelFormat.Format32bppBGR,
197 SharpDX.WIC.BitmapCreateCacheOption.CacheOnLoad );
199 catch( SharpDX.SharpDXException e )
201 Log.ERROR( $"描画先 WIC ビットマップの作成に失敗しました。(0x{e.HResult:x8})[{変数付きファイルパス}]" );
207 public void Dispose()
211 this.デコードタスク?.Wait(); // デコードタスクは、デコーダを起動してない場合は null になる。
213 this.キューが空いた.Close();
214 this.タスクを終了せよ.Close();
217 QueueItem item = null;
218 while( ( 0 < this.SampleQueue.Count ) && ( this.SampleQueue.TryDequeue( out item ) ) )
220 // キューの中の各アイテムのCOM参照カウントは 1 なので、それぞれ 1 回だけ Dispose すればいい。
224 FDK.Utilities.解放する( ref this.WICビットマップ );
225 FDK.Utilities.解放する( ref this.MediaType );
226 FDK.Utilities.解放する( ref this.SourceReaderEx );
228 public void デコードキャッシング()
230 // 最初のほうのサンプルのデコードには 100~200 ms ほどかかってしまうので、あらかじめ少しデコードしてメモリにキャッシュさせることでこれを緩和する。
233 for( int i = 0; i < 60; i++ ) // 60フレームもあれば、2つめのキーフレームくらいには届くだろう……
235 #region " SourceReader から次のサンプル(フレーム)を1つ取得しては解放する。"
237 var sample = (SharpDX.MediaFoundation.Sample) null;
239 var ストリームフラグ = SharpDX.MediaFoundation.SourceReaderFlags.None;
240 long サンプルの時刻100ns = 0;
243 sample = this.SourceReaderEx.ReadSample(
244 SharpDX.MediaFoundation.SourceReaderIndex.FirstVideoStream,
245 SharpDX.MediaFoundation.SourceReaderControlFlags.None,
250 catch( SharpDX.SharpDXException e )
252 Log.ERROR( $"SourceReaderEx.ReadSample() に失敗しました。(0x{e.HResult:x8})" );
264 // SourceReader を先頭へリセット。
265 this.SourceReaderEx.SetCurrentPosition( 0 );
267 Log.Info( $"{Utilities.現在のメソッド名}: 動画のセットアップ(フレームキャッシング)を行いました。" );
269 public void デコードを開始する()
271 this.デコードタスク = Task.Factory.StartNew( this.デコードタスクエントリ );
274 /// 呼び出し元は、取得したサンプルを使い終わったら Dispose すること。
276 public bool キューからビットマップを取り出す( out SharpDX.Direct2D1.Bitmap D2Dビットマップ, out double 再生時刻sec )
278 QueueItem item = null;
279 if( ( 0 == this.SampleQueue.Count ) || ( false == this.SampleQueue.TryDequeue( out item ) ) )
283 return false; // キューが空だったか、Dequeue が一歩遅かった?(ないはずだが
286 D2Dビットマップ = item.D2Dビットマップ; // キューから出しても、代入しても、COMの参照カウントは変化しない。
287 再生時刻sec = item.再生時刻sec;
289 this.前フレームの時刻sec = item.再生時刻sec;
290 this.キューが空いた.Set(); // キューが空いたので、このイベントを set する。
294 public bool 次のビットマップの有無を確認する( out SharpDX.Direct2D1.Bitmap D2Dビットマップ, out double 再生時刻sec )
296 QueueItem item = null;
297 if( ( 0 == this.SampleQueue.Count ) || ( false == this.SampleQueue.TryPeek( out item ) ) ) // キューから取り出さない。
301 return false; // キューが空だったか、Peek が一歩遅かった?(ないはずだが
304 D2Dビットマップ = item.D2Dビットマップ; // 代入しても、COMの参照カウントは変化しない。
305 再生時刻sec = item.再生時刻sec;
309 protected class QueueItem : IDisposable
311 public double 再生時刻sec = 0; // 動画の先頭を 0 とする時刻。秒単位。
312 public SharpDX.Direct2D1.Bitmap D2Dビットマップ = null;
314 public void Dispose()
316 FDK.Utilities.解放する( ref this.D2Dビットマップ );
319 protected ConcurrentQueue<QueueItem> SampleQueue = new ConcurrentQueue<QueueItem>();
320 protected SharpDX.MediaFoundation.SourceReaderEx SourceReaderEx;
321 protected SharpDX.MediaFoundation.MediaType MediaType;
322 protected bool ループ再生する = false;
323 protected double 前フレームの時刻sec = -1.0;
324 protected System.Threading.Tasks.Task デコードタスク = null;
325 protected ManualResetEvent キューが空いた = new ManualResetEvent( true );
326 protected AutoResetEvent タスクを終了せよ = new AutoResetEvent( false );
327 protected SharpDX.WIC.Bitmap WICビットマップ = null; // MediaFoundation は WICBitmap に出力する。
329 protected void デコードタスクエントリ()
331 var events = new WaitHandle[ 2 ];
332 events[ 0 ] = this.キューが空いた;
333 events[ 1 ] = this.タスクを終了せよ;
335 while( WaitHandle.WaitAny( events ) != 1 ) // WaitAny() は、シグナル状態になったハンドルの配列インデックスを返す。
337 // キューが空いてるので、サンプルを1つデコードして格納する。
338 if( this.サンプルをひとつ取得してキューへ格納する() )
340 // キューがいっぱいになったら、空くまで待つ。
341 if( キューのサイズ == this.SampleQueue.Count )
342 this.キューが空いた.Reset(); // 空いたらこれが set される。
346 break; // エラーまたはストリーム終了 → デコードタスクを終了する。
350 protected bool サンプルをひとつ取得してキューへ格納する()
352 var sample = (SharpDX.MediaFoundation.Sample) null;
353 var bitmap = (SharpDX.Direct2D1.Bitmap) null;
354 long 再生時刻100ns = 0; // 100ns 単位
358 #region " ソースリーダーから次のサンプル(フレーム)を1つ取得する。"
360 var ストリームフラグ = SharpDX.MediaFoundation.SourceReaderFlags.None;
363 sample = this.SourceReaderEx.ReadSample(
364 SharpDX.MediaFoundation.SourceReaderIndex.FirstVideoStream,
365 SharpDX.MediaFoundation.SourceReaderControlFlags.None,
371 #region " 取得結果フラグに応じて、必要な処理があれば行なう。"
372 //---------------------------------------------------
373 if( ストリームフラグ.HasFlag( SharpDX.MediaFoundation.SourceReaderFlags.Endofstream ) ) // BOX化コストとか気にしない
380 FDK.Log.Info( "動画をループ再生します。" );
381 this.ループした際の先頭時刻sec = this.前フレームの時刻sec; // EndOfStream フラグがセットされたときは、ReadSample() で返されるサンプル時刻は無効(0 になっている)。
382 this.SourceReaderEx.SetCurrentPosition( 0 ); // ストリーム再生位置を先頭へ。
383 return this.サンプルをひとつ取得してキューへ格納する(); // 再帰で先頭サンプルを取得して返す。
388 FDK.Log.Info( "動画の再生を終了します。" );
394 //else if( ストリームフラグ.HasFlag( SharpDX.MediaFoundation.SourceReaderFlags.Newstream ) )
397 //else if( ストリームフラグ.HasFlag( SharpDX.MediaFoundation.SourceReaderFlags.Nativemediatypechanged ) )
400 //else if( ストリームフラグ.HasFlag( SharpDX.MediaFoundation.SourceReaderFlags.Currentmediatypechanged ) )
403 //else if( ストリームフラグ.HasFlag( SharpDX.MediaFoundation.SourceReaderFlags.StreamTick ) )
406 //else if( ストリームフラグ.HasFlag( SharpDX.MediaFoundation.SourceReaderFlags.AllEffectsremoved ) )
409 else if( ストリームフラグ.HasFlag( SharpDX.MediaFoundation.SourceReaderFlags.Error ) )
413 FDK.Log.Info( $"エラーが発生したので、動画の再生を終了します。" );
414 throw new SharpDX.SharpDXException( SharpDX.Result.Fail );
418 //---------------------------------------------------
421 if( this.現在の再生時刻100ns() < 再生時刻100ns ) // 現時点でもう再生時刻を超えてるなら、スキップする。
423 this.サンプルをD2Dビットマップに転送する( sample, out bitmap );
426 this.SampleQueue.Enqueue( new QueueItem() {
427 D2Dビットマップ = bitmap, // COM参照カウントは変化しない。
428 再生時刻sec = (double) ( 再生時刻100ns / ( 10.0 * 1000.0 * 1000.0 ) ), // 100ns単位 → 秒単位へ変換。
434 FDK.Log.Info( $"エラーが発生したので、動画の再生を終了します。[{e.Message}]" );
445 private int キューのサイズ = 16;
446 private デバイスリソース dr = null;
448 private unsafe void サンプルをD2Dビットマップに転送する( SharpDX.MediaFoundation.Sample サンプル, out SharpDX.Direct2D1.Bitmap D2Dビットマップ )
450 var buffer = (SharpDX.MediaFoundation.MediaBuffer) null;
451 var buffer2d2 = (SharpDX.MediaFoundation.Buffer2D2) null;
454 #region " サンプルからサンプルバッファ (MediaBuffer) を取得する。"
458 buffer = サンプル.ConvertToContiguousBuffer();
462 Log.ERROR( $"サンプルバッファの取得に失敗しました。(0x{e.HResult:x8})" );
467 #region " サンプルバッファを Buffer2D2 にキャストする。"
471 buffer2d2 = buffer.QueryInterface<SharpDX.MediaFoundation.Buffer2D2>();
473 catch( SharpDX.SharpDXException e )
475 Log.ERROR( $"サンプルバッファから Buffer2D2 へのキャストに失敗しました。(0x{e.HResult:x8})" );
480 #region " サンプルバッファをロックする。"
482 byte[] scanLine0_bp = new byte[ 8 ]; // 「生ポインタ」が格納される。32bitなら[0~3]、64bitなら[0~7]が有効。(CPUではなく.NETに依存)
484 byte[] bufferStart_bp = new byte[ 8 ]; // 「生ポインタ」が格納される。こちらは使わない。
488 buffer2d2.Lock2DSize(
489 SharpDX.MediaFoundation.Buffer2DLockFlags.Read,
495 catch( SharpDX.SharpDXException e )
497 Log.ERROR( $"サンプルバッファのロックに失敗しました。(0x{e.HResult:x8})" );
504 #region " サンプルバッファのネイティブ先頭アドレスを取得する。"
506 byte* scanLine0 = null;
509 scanLine0 = (byte*) this.生ポインタを格納したbyte配列からポインタを取得して返す( scanLine0_bp );
511 catch( SharpDX.SharpDXException e )
513 Log.ERROR( $"サンプルバッファのアドレスの取得に失敗しました。(0x{e.HResult:x8})" );
518 #region " サンプルから WICBitmap を生成する。"
522 // 描画先WICビットマップをロックする。
523 using( var bitmapLock = this.WICビットマップ.Lock(
524 new SharpDX.Rectangle( 0, 0, this.WICビットマップ.Size.Width, this.WICビットマップ.Size.Height ),
525 SharpDX.WIC.BitmapLockFlags.Write ) )
527 // サンプルバッファからWICビットマップへ、ARGB32 を G8B8R8X8 に変換しながらコピー。
528 int bitmapStride = bitmapLock.Stride;
529 byte* src = scanLine0;
530 byte* dest = (byte*) bitmapLock.Data.DataPointer.ToPointer();
532 for( int y = 0; y < this.サイズdpx.Height; y++ )
534 // ARGB32 to G8B8R8X8 ではデータ変換が不要なので、一行を一気にコピー。
535 動画デコーダ.CopyMemory( dest, src, this.サイズdpx.Width * 4 ); // ARGB=4バイト。
537 dest += bitmapStride; // bitmapStride は byte 単位
541 catch( SharpDX.SharpDXException e )
543 Log.ERROR( $"WICビットマップの Lock に失敗しました。(0x{e.HResult:x8})" );
548 Log.ERROR( $"サンプルバッファから WIC ビットマップへのデータの転送に失敗しました。(0x{e.HResult:x8})" );
553 #region " WICビットマップからD2Dビットマップを生成する。"
557 D2Dビットマップ = SharpDX.Direct2D1.Bitmap.FromWicBitmap(
561 catch( SharpDX.SharpDXException e )
563 Log.ERROR( $"D2Dビットマップの作成に失敗しました。(0x{e.HResult:x8})" );
571 #region " サンプルバッファのロックを解除する。"
573 buffer2d2.Unlock2D();
580 FDK.Utilities.解放する( ref buffer2d2 );
581 FDK.Utilities.解放する( ref buffer );
584 private unsafe void* 生ポインタを格納したbyte配列からポインタを取得して返す( byte[] 生ポインタ )
586 if( ( 4 == IntPtr.Size ) && System.BitConverter.IsLittleEndian )
588 // (A) 32bit, リトルエンディアン
590 for( int i = 0; i < 4; i++ )
591 生アドレス32bit += ( (int) 生ポインタ[ i ] ) << ( i * 8 );
592 return new IntPtr( 生アドレス32bit ).ToPointer();
594 else if( ( 8 == IntPtr.Size ) && System.BitConverter.IsLittleEndian )
596 // (B) 64bit, リトルエンディアン
598 for( int i = 0; i < 8; i++ )
599 生アドレス64bit += ( (int) 生ポインタ[ i ] ) << ( i * 8 );
600 return new IntPtr( 生アドレス64bit ).ToPointer();
602 else if( ( 4 == IntPtr.Size ) && ( false == System.BitConverter.IsLittleEndian ) )
604 // (C) 32bit, ビッグエンディアン
606 for( int i = 0; i < 4; i++ )
607 生アドレス32bit += ( (int) 生ポインタ[ 4 - i ] ) << ( i * 8 );
608 return new IntPtr( 生アドレス32bit ).ToPointer();
610 else if( ( 8 == IntPtr.Size ) && ( false == System.BitConverter.IsLittleEndian ) )
612 // (D) 64bit, ビッグエンディアン
614 for( int i = 0; i < 8; i++ )
615 生アドレス64bit += ( (int) 生ポインタ[ 8 - i ] ) << ( i * 8 );
616 return new IntPtr( 生アドレス64bit ).ToPointer();
619 throw new SharpDX.SharpDXException( SharpDX.Result.NotImplemented, "この .NET アーキテクチャには対応していません。" );
624 private double bs_ループした際の先頭時刻sec = 0.0;
625 private SharpDX.Size2 bs_サイズdpx = new SharpDX.Size2( 1, 1 );
629 #region " Win32 API "
631 [System.Runtime.InteropServices.DllImport( "kernel32.dll", SetLastError = true )]
632 private static extern unsafe void CopyMemory( void* dst, void* src, int size );