D言語で動画再生

# はじめに
[D言語アドベントカレンダー2020](https://qiita.com/advent-calendar/2020/dlang) 12日目の記事（今回）です。
[5日目の記事](https://qiita.com/devmynote/items/ad11d2e4b9156cf15778)（前回）の続編的な内容です。

前回の記事では、音楽再生を実装しました。
今回の記事では、続けて、動画再生を実装します。
開発環境の構築情報は前回と同じため、今回は省略しています。興味がある方は、前回の記事を参照してください。

# D言語の特徴
前回と今回のソースコードを書いた際にあらためて実感した、D言語の特徴を２つ紹介します。

## ABI互換
D言語は、C言語との[ABI](https://ja.wikipedia.org/wiki/%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%83%90%E3%82%A4%E3%83%8A%E3%83%AA%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%95%E3%82%A7%E3%83%BC%E3%82%B9)互換があります。
つまり、本来C言語での開発用に作成された`FFmpeg`や`SDL`のライブラリファイルをD言語のコンパイル時にそのまま利用できます。また、`FFmpeg`や`SDL`の`API`について、C言語向けの解説をD言語でも活用できます。
この記事は、実際の活用例になります。

## スレッド局所記憶
D言語では、[スレッド局所記憶](https://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%AC%E3%83%83%E3%83%89%E5%B1%80%E6%89%80%E8%A8%98%E6%86%B6#D%E8%A8%80%E8%AA%9E)を採用しています。
D言語のグローバル変数はデフォルトではスレッド局所変数のため、[スレッドセーフ](https://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%AC%E3%83%83%E3%83%89%E3%82%BB%E3%83%BC%E3%83%95)です。
ただ、今回のソースコードでは、コールバック関数などの別スレッドとグローバル変数を共有するために、`__gshared`属性を付けています。
スレッド局所にするかどうかをグローバル変数ごとに選択できるのは、いいですよね。

# 環境
* OS : Windows 10 64ビット版 (バージョン 1909)
* D言語コンパイラ : DMD v2.094.0
* 使用するライブラリ
  * SDL 2.0.12 (bindbc-sdl バージョン 0.19.2)
  * FFMpeg 4.3.1 (ffmpeg4d バージョン 4.1.0)

# ソースコード
```d:app.d
import std.stdio;
import std.utf;
import core.stdc.stdio;
import core.sys.windows.windows;

import bindbc.sdl;
import bindbc.sdl.image;
import ffmpeg;

pragma(lib, "User32.lib");
pragma(lib, "Winmm.lib");

__gshared AVFormatContext* format_context;
__gshared AVStream* audio_stream;
__gshared AVStream* video_stream;
__gshared AVCodecContext* codec_audio;
__gshared AVCodecContext* codec_video;

__gshared SDL_Window* window;

__gshared ubyte*[] videoBuf;
__gshared ulong[]  videoPts;
__gshared int width;
__gshared int height;
__gshared bool startPlay = false;

__gshared SwrContext* swr = null;
__gshared ubyte[] audioBuf;
__gshared ubyte[] waveBuf;
__gshared int read_buf;
__gshared int play_buf;

void drawScreen()
{
	if ( videoBuf.length < 1 ){
		return;
	}
	ubyte* buffer = videoBuf[0];
	int w0 = (width + 7) / 8 * 8;
	SDL_Surface* image = SDL_CreateRGBSurfaceWithFormatFrom(
		cast(void *)buffer, w0, height, 24, w0 * 3, SDL_PIXELFORMAT_BGR24);
	
	SDL_Surface* screen = SDL_GetWindowSurface(window);
	SDL_BlitSurface(image, null, screen, null);
	SDL_UpdateWindowSurface(window);
	
	SDL_FreeSurface(image);
	av_free(buffer);
	videoBuf = videoBuf[1..$];
	videoPts = videoPts[1..$];
}

void cacheVideo(AVFrame* frame)
{
	if ( width == 0 ){
		width  = frame.width;
		if ( video_stream.sample_aspect_ratio.num > 0 ){
			width = width * video_stream.sample_aspect_ratio.num / video_stream.sample_aspect_ratio.den;
		}
		height = frame.height;
		initWindow(width, height);
	}
	int w0 = (width + 7) / 8 * 8;
	ulong size = avpicture_get_size(AVPixelFormat.AV_PIX_FMT_BGR24, w0, frame.height);
	ubyte* buffer = cast(ubyte*)av_malloc(size);
	
	AVFrame* pFrameRGB = av_frame_alloc();
	avpicture_fill(cast(AVPicture*)pFrameRGB, buffer, AVPixelFormat.AV_PIX_FMT_BGR24, w0, frame.height);
	SwsContext* pSWSCtx = sws_getContext(
		frame.width, frame.height, cast(AVPixelFormat)frame.format,
			  width, frame.height, AVPixelFormat.AV_PIX_FMT_BGR24,
		SWS_BICUBIC, null, null, null);
	sws_scale(pSWSCtx, cast(ubyte**)frame.data, cast(int*)frame.linesize, 0,
		frame.height, cast(ubyte**)pFrameRGB.data, cast(int*)pFrameRGB.linesize);
	sws_freeContext(pSWSCtx);
	av_free(pFrameRGB);
	
	videoBuf ~= buffer;
	videoPts ~= frame.pts;
}

void decodeVideo(AVPacket *packet)
{
	AVFrame* frame = av_frame_alloc();
	if ( avcodec_send_packet(codec_video, packet) != 0 ){
		writeln("avcodec_send_packet failed");
	}
	while ( avcodec_receive_frame(codec_video, frame) == 0 ){
		cacheVideo(frame);
	}
	if ( packet != null ){
		av_packet_unref(packet);
	}
	av_frame_free(&frame);
}

extern(Windows) nothrow
LRESULT playProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch ( message ){
		case MM_WOM_DONE:
				play_buf++;
			return ( 0 );
		case MM_WOM_OPEN:
			startPlay = true;
			return ( 0 );
		case WM_DESTROY:
			PostQuitMessage(0);
			return ( 0 );
		default:
	}
	return ( DefWindowProc(hwnd, message, wParam, lParam) );
}

void initSwr(AVFrame* frame)
{
	swr = swr_alloc();
	if ( swr == null ){
		writeln("swr_alloc failed");
	}
	av_opt_set_int(swr, "in_channel_layout", frame.channel_layout, 0);
	av_opt_set_int(swr, "out_channel_layout", frame.channel_layout, 0);
	av_opt_set_int(swr, "in_sample_rate", frame.sample_rate, 0);
	av_opt_set_int(swr, "out_sample_rate", frame.sample_rate, 0);
	av_opt_set_sample_fmt(swr, "in_sample_fmt", cast(AVSampleFormat)frame.format, 0);
	av_opt_set_sample_fmt(swr, "out_sample_fmt", AVSampleFormat.AV_SAMPLE_FMT_S16, 0);
	int ret = swr_init(swr);
	if ( ret < 0 ){
		writefln("swr_init failed : %08x", AVERROR(ret));
	}
}

void quitSwr()
{
	swr_free(&swr);
}

const BUFSIZE = 44100 * 4 * 1;		// 1second
__gshared HWAVEOUT   hwaveout;
__gshared WAVEHDR[2] waveheader;

void initWave(AVFrame* frame)
{
	PCMWAVEFORMAT waveformat;
	waveformat.wf.wFormatTag = WAVE_FORMAT_PCM;
	waveformat.wf.nChannels = cast(ushort)frame.channels;
	waveformat.wf.nSamplesPerSec = frame.sample_rate;
	waveformat.wf.nAvgBytesPerSec = frame.sample_rate * 2;
	waveformat.wf.nBlockAlign = cast(ushort)(frame.channels * 2);
	waveformat.wBitsPerSample = 16;
	
	int ret;
	ret = waveOutOpen(&hwaveout, WAVE_MAPPER, cast(WAVEFORMATEX*)&waveformat, cast(DWORD_PTR)&playProc, 0, CALLBACK_FUNCTION);
	if ( ret != 0 ){
		writefln("waveOutOpen failed : %d", ret);
	}
	
	waveBuf.length = BUFSIZE * 2;
	for ( int i = 0; i < 2; i++ ){
		with ( waveheader[i] ){
			lpData = (cast(char*)waveBuf.ptr) + (i * BUFSIZE);
			dwBufferLength = BUFSIZE;
			dwFlags = 0;
			dwLoops = 0;
		}
		ret = waveOutPrepareHeader(hwaveout, &waveheader[i], WAVEHDR.sizeof);
		if ( ret != 0 ){
			writefln("waveOutPrepareHeader failed : %d", ret);
		}
	}
}

void playAudio(uint idx1, uint idx2)
{
	uint base = read_buf % 2;
	waveBuf[base * BUFSIZE .. base * BUFSIZE + idx2 - idx1] = audioBuf[idx1 .. idx2];
	int ret;
	ret = waveOutWrite(hwaveout, &waveheader[base], WAVEHDR.sizeof);
	if ( ret != 0 ){
		writefln("waveOutWrite failed : %d", ret);
	}
	read_buf++;
}

void watchAudioCache()
{
	if ( play_buf + 1 == read_buf ){
		if ( audioBuf.length > (read_buf + 1) * BUFSIZE ){
			playAudio(read_buf * BUFSIZE, (read_buf + 1) * BUFSIZE);
		}
	} else
	if ( play_buf == read_buf ){
		if ( audioBuf.length > (read_buf + 1) * BUFSIZE ){
			playAudio(read_buf * BUFSIZE, (read_buf + 1) * BUFSIZE);
		}
	}
}

void cacheAudio(AVFrame* frame)
{
	if ( swr == null ){
		initSwr(frame);
		initWave(frame);
	}
	int swr_buf_len = frame.nb_samples * frame.channels * 2;
	audioBuf.length += swr_buf_len;
	ubyte* swr_bufp = audioBuf.ptr;
	swr_bufp += audioBuf.length - swr_buf_len;
	
	int ret = swr_convert(swr, &swr_bufp, frame.nb_samples, cast(const ubyte**)frame.extended_data, frame.nb_samples);
	if ( ret < 0 ){
		writefln("swr_convert failed : %d", AVERROR(ret));
	}
}

void decodeAudio(AVPacket *packet)
{
	AVFrame* frame = av_frame_alloc();
	if ( avcodec_send_packet(codec_audio, packet) != 0 ){
		writeln("avcodec_send_packet failed");
	}
	while ( avcodec_receive_frame(codec_audio, frame) == 0 ){
		cacheAudio(frame);
	}
	if ( packet != null ){
		av_packet_unref(packet);
	}
	av_frame_free(&frame);
}

bool readFrame()
{
	AVPacket packet = AVPacket();
	if ( av_read_frame(format_context, &packet) == 0 ){
		if ( packet.stream_index == audio_stream.index ){
			decodeAudio(&packet);
		} else
		if ( packet.stream_index == video_stream.index ){
			decodeVideo(&packet);
		}
	} else {
		// flush decoder
		decodeAudio(null);
		decodeVideo(null);
		return ( true );
	}
	return ( false );
}

int main(string[] args)
{
	if ( args.length < 2 ){
	    writeln("Argument required : input file");
		return ( 1 );
	}
	initFFMpeg(args[1]);
	scope(exit) quitFFMpeg();
	initSDL();
	scope(exit) quitSDL();
	
	bool endRead = false;
	bool endPlay = false;
	ulong time1 = av_gettime_relative();
	while ( !endPlay ){
		ulong time2 = av_gettime_relative();
		if ( !endRead ){
			if ( videoBuf.length < 60 ){	// Whether to wait
				endRead = readFrame();
			}
		}
		watchAudioCache();
		// Event Check
		SDL_Event event;
		while ( SDL_PollEvent(&event) ){
			switch ( event.type ){
				case SDL_KEYDOWN:
				case SDL_QUIT:
					endPlay = true;
					continue;
				default:
			}
		}
		if ( !startPlay ){
			time1 = time2;
		} else {
			ulong p = (videoPts.length == 0) ? 0 : videoPts[0];
			ulong vtime = 1_000_000 * p
				* video_stream.time_base.num / video_stream.time_base.den;
			if ( time1 + vtime < time2 ){	// Whether to wait
				drawScreen();
			}
		}
	}
	quitSwr();
	waveOutClose(hwaveout);
	
	return ( 0 );
}

void initFFMpeg(string fileName)
{
	// FFmpeg initialize
	av_register_all();
	// AVFormatContext initialize
	const char* input_path = toUTFz!(const(char)*)(fileName);
	if ( avformat_open_input(&format_context, input_path, null, null) != 0 ){
		writeln("avformat_open_input failed");
		writefln("%s", fileName);
	}
	// AVStream initialize
	if ( avformat_find_stream_info(format_context, null) < 0 ){
		writeln("avformat_find_stream_info failed");
	}
	for ( int i = 0; i < cast(int)format_context.nb_streams; ++i ){
		if ( format_context.streams[i].codecpar.codec_type == AVMediaType.AVMEDIA_TYPE_AUDIO ){
			audio_stream = format_context.streams[i];
			codec_audio = getCodecContext(audio_stream);
		} else
		if ( format_context.streams[i].codecpar.codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO ){
			video_stream = format_context.streams[i];
			codec_video = getCodecContext(video_stream);
		}
	}
	if ( audio_stream == null ){
		writeln("No audio stream ...");
	}
	if ( video_stream == null ){
		writeln("No video stream ...");
	}
}

AVCodecContext* getCodecContext(AVStream* stream)
{
	AVCodec* codec = avcodec_find_decoder(stream.codecpar.codec_id);
	if ( codec == null ){
		writeln("No supported decoder ...");
	}
	AVCodecContext* codec_context = avcodec_alloc_context3(codec);
	if ( codec_context == null ){
		writeln("avcodec_alloc_context3 failed");
	}
	if ( avcodec_parameters_to_context(codec_context, stream.codecpar) < 0 ){
		writeln("avcodec_parameters_to_context failed");
	}
	if ( avcodec_open2(codec_context, codec, null) != 0 ){
		writeln("avcodec_open2 failed");
	}
	
	return ( codec_context );
}

void quitFFMpeg()
{
	avcodec_free_context(&codec_audio);
	avcodec_free_context(&codec_video);
	avformat_close_input(&format_context);
}

void initSDL()
{
	// Load SDL
	SDLSupport ret = loadSDL();
	if ( ret != sdlSupport ){
		if ( ret == SDLSupport.noLibrary ){
			writeln("SDLSupport.noLibrary");
		} else if ( SDLSupport.badLibrary ){
			writeln("SDLSupport.badLibrary");
		}
	}
	// Initialize SDL
	if ( SDL_Init(SDL_INIT_VIDEO) < 0 ){
		writefln("SDL_Init failed : %s", SDL_GetError());
	}
	// Load SDLImage
	immutable sdlImageVersion = loadSDLImage();
	IMG_Init(IMG_INIT_JPG | IMG_INIT_PNG);
}

void initWindow(int wid, int hei)
{
	window = SDL_CreateWindow("FFMPEG PLAYER",
		SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
		wid, hei, SDL_WINDOW_SHOWN);
	if ( window == null ){
		writefln("Window could not be created! SDL_Error: %s", SDL_GetError());
	}
}

void quitSDL()
{
	SDL_DestroyWindow(window);
	IMG_Quit();
	unloadSDLImage();
	SDL_Quit();
	unloadSDL();
}
```

```json:dub.json
{
	"name": "mplayer",
	"description": "video player",
	"authors": [
		"devmynote"
	],
	"license": "BSL-1.0",
	"dependencies": {
		"ffmpeg4d": "~>4.1.0",
		"bindbc-sdl": "~>0.19.2",
	},
	"versions": ["SDL_205","BindSDL_Image"],
	"lflags":["/libpath:./lib"],
}
```
# ソースコード補足説明
シーク機能やボリューム調整機能などがないシンプルな動画再生処理となります。おかげで400行弱に収まりました。
`ffmpeg`の大まかな使い方を調べるには、行数が少ない分、わかりやすいのではと思います。
グローバル変数だらけのソースコードで、なんかすいません・・・

音楽データも画像データも`ffmpeg`を通して、`AVFrame`単位で取得できるところは共通です。
そのあと、音楽データは`WAVE_FORMAT_PCM`形式に変換し、`Windows API`を使って再生しています。
画像データは、`AV_PIX_FMT_BGR24`形式に変換し、`SDL`を使って再生しています。

実装した各関数を簡単に説明します。

|関数名|関数の簡単な説明|
|:--|:--|
|drawScreen|グローバル変数に保存した画像データを画面に表示します。|
|cacheVideo|引数であるframeの画像データをAV_PIX_FMT_BGR24形式に変換し、グローバル変数に保存します。画像表示と音楽再生の同期をとるためにduration情報を取得します。|
|decodeVideo|packetからframe情報を取得し、後続処理に渡しています。|
|playProc|音楽再生のコールバック関数です。音楽再生の終了を意味するMM_WOM_DONEメッセージを受け取ると、続きの音楽データを再生するようplay_bufをインクリメントします。|
|initSwr|SwrContextを初期化します。|
|quitSwr|SwrContextを開放します。|
|initWave|音楽再生用PCMWAVEFORMATを初期化します。|
|playAudio|グローバル変数に保存した音楽データを再生します。|
|watchAudioCache|play_bufの値をみて、音楽再生処理を行います。再生途中で音が途切れないようダブルバッファを実装しています|
|cacheAudio|引数であるframeの音楽データをWAVE_FORMAT_PCM形式に変換し、グローバル変数に保存します。|
|decodeAudio|packetからframe情報を取得し、後続処理に渡しています。|
|readFrame|packetを取得し、後続処理に渡しています。packet.stream_indexの値をみて、音楽処理、画像処理を振り分けます。|
|main|ファイル名である引数をチェックし、ファイルを読み込みます。画像表示と音楽再生の同期をとります。ウィンドウの制御を行います。|
|initFFMpeg|FFMpegライブラリを初期化します。|
|getCodecContext|FFMpeg APIの定型的な処理をまとめています。|
|quitFFMpeg|FFMpegライブラリを開放します。|
|initSDL|SDLライブラリを初期化します。|
|initWindow|画面に表示するウィンドウを初期化します。|
|quitSDL|SDLライブラリを開放します。|

# コンパイル
私の開発環境を例にとると、`D:\Dev`が開発用フォルダです。
アプリケーション作成用に`mplayer`フォルダを作成しています。

```text:
D:\Dev> cd mplayer
```
`mplayer`内のフォルダ構成は`tree`コマンドの出力通りです。

```text:
D:\Dev\mplayer> tree
フォルダー パスの一覧:  ボリューム ****
ボリューム シリアル番号は ****-**** です
D:.
├─lib
└─source
```

`D:\Dev\mplayer`フォルダに`dub.json`と`DLL`ファイルを配置します。
`DLL`ファイルは、ダウンロードした`SDL2-2.0.12-win32-x64.zip`と`ffmpeg-release-full-shared.7z`にあります。

```text:コマンドプロンプト
D:\Dev\mplayer> dir /B
avcodec-58.dll
avdevice-58.dll
avfilter-7.dll
avformat-58.dll
avutil-56.dll
dub.json
lib
postproc-55.dll
SDL2.dll
source
swresample-3.dll
swscale-5.dll
```
`D:\Dev\mplayer\lib`フォルダに`ffmpeg-release-full-shared.7z`のlibファイルを配置します。

```text:コマンドプロンプト
D:\Dev\mplayer> dir /B lib
avcodec.lib
avdevice.lib
avfilter.lib
avformat.lib
avutil.lib
postproc.lib
swresample.lib
swscale.lib
```
`D:\Dev\mplayer\source`フォルダに`app.d`を配置します。

```text:コマンドプロンプト
D:\Dev\mplayer> dir /B source
app.d
```
各ファイルの配置が完了したら、`DUB`コマンドでコンパイルを実行します。

```text:コマンドプロンプト
D:\Dev\mplayer> dub build --build=release --arch=x86_64
Performing "release" build using D:\App\Dev\dmd2\windows\bin\dmd.exe for x86_64.
bindbc-loader 0.3.2: target for configuration "noBC" is up to date.
bindbc-sdl 0.19.2: target for configuration "dynamic" is up to date.
mplayer ~master: building configuration "application"...
Linking...
To force a rebuild of up-to-date targets, run again with --force.
```

# 実行例
コンパイルが無事終了すると、`D:\Dev\mplayer`フォルダに`mplayer.exe`が生成されます。
再生したい動画ファイル名を引数に指定して実行することで、動画再生ができます。

```text:
D:\Dev\mplayer> mplayer "再生したい動画ファイル名"
```

# おまけ
今回作成した動画再生処理ソースコードの副産物として、画像表示処理のみ版も作りましたので、おまけで紹介します。
画像処理部分だけが抜粋されているので、その部分だけを絞って見たい方の参考になればと思います。
ウェイト処理を入れていないので、音無し、早送りの動画ビューアのような動きになります。

```d:app.d
import std.stdio;
import std.utf;
import core.stdc.string;

import bindbc.sdl;
import bindbc.sdl.image;
import ffmpeg;

int main(string[] args)
{
	if ( args.length < 2 ){
	    writeln("Argument required : input file");
		return ( 1 );
	}
	AVFormatContext* format_context;
	AVStream* video_stream;
	AVCodecContext* codec_context;
	initContext(args[1], &format_context, &video_stream, &codec_context);
	playMedia(format_context, video_stream, codec_context);
	avcodec_free_context(&codec_context);
	avformat_close_input(&format_context);
	
	return ( 0 );
}

void initContext(string fileName, AVFormatContext** pfctx, AVStream** pstream, AVCodecContext** pcctx)
{
	// FFmpeg initialize
	av_register_all();
	// AVFormatContext initialize
	AVFormatContext* format_context;
	const char* input_path = toUTFz!(const(char)*)(fileName);
	if ( avformat_open_input(&format_context, input_path, null, null) != 0 ){
		writeln("avformat_open_input failed");
		writefln("%s", fileName);
	}
	if ( avformat_find_stream_info(format_context, null) < 0 ){
		writeln("avformat_find_stream_info failed");
	}
	// AVStream initialize
	AVStream* video_stream = null;
	for ( int i = 0; i < cast(int)format_context.nb_streams; ++i ){
		if ( format_context.streams[i].codecpar.codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO ){
			video_stream = format_context.streams[i];
			break;
		}
	}
	if ( video_stream == null ){
		writeln("No video stream ...");
	}
	// AVCodecContext initialize
	AVCodec* codec = avcodec_find_decoder(video_stream.codecpar.codec_id);
	if ( codec == null ){
		writeln("No supported decoder ...");
	}
	AVCodecContext* codec_context = avcodec_alloc_context3(codec);
	if ( codec_context == null ){
		writeln("avcodec_alloc_context3 failed");
	}
	if ( avcodec_parameters_to_context(codec_context, video_stream.codecpar) < 0 ){
		writeln("avcodec_parameters_to_context failed");
	}
	if ( avcodec_open2(codec_context, codec, null) != 0 ){
		writeln("avcodec_open2 failed");
	}
	// return value
	*pfctx   = format_context;
	*pstream = video_stream;
	*pcctx   = codec_context;
}

void playMedia(AVFormatContext* format_context, AVStream* video_stream, AVCodecContext* codec_context)
{
	// Load SDL
	SDLSupport ret = loadSDL();
	if ( ret != sdlSupport ){
		if ( ret == SDLSupport.noLibrary ){
			writeln("SDLSupport.noLibrary");
		} else if ( SDLSupport.badLibrary ){
			writeln("SDLSupport.badLibrary");
		}
	}
	scope(exit) unloadSDL();
	// Initialize SDL
	if ( SDL_Init(SDL_INIT_VIDEO) < 0 ){
		writefln("SDL_Init failed : %s", SDL_GetError());
	}
	scope(exit) SDL_Quit();
	// Load SDLImage
	immutable sdlImageVersion = loadSDLImage();
	IMG_Init(IMG_INIT_JPG | IMG_INIT_PNG);
	scope(exit) unloadSDLImage();
	
	// Create window
	int scrWidth  = codec_context.width;
	if ( video_stream.sample_aspect_ratio.num > 0 ){
		scrWidth = scrWidth * video_stream.sample_aspect_ratio.num / video_stream.sample_aspect_ratio.den;
	}
	SDL_Window* window = SDL_CreateWindow("Video fast-forward",
		SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
		scrWidth, codec_context.height, SDL_WINDOW_SHOWN);
	if ( window == null ){
		writefln("Window could not be created! SDL_Error: %s", SDL_GetError());
	}
	scope(exit) SDL_DestroyWindow(window);
	// Play video
	AVFrame* frame = av_frame_alloc();
	scope(exit) av_frame_free(&frame);
	AVPacket packet = AVPacket();
	PLAY_LOOP: while ( av_read_frame(format_context, &packet) == 0 ){
		if ( packet.stream_index == video_stream.index ){
			if ( avcodec_send_packet(codec_context, &packet) != 0 ){
				writeln("avcodec_send_packet failed");
			}
			while ( avcodec_receive_frame(codec_context, frame) == 0 ){
				drawWindow(frame, window, scrWidth);
			}
		}
		av_packet_unref(&packet);
		
		// Event Check
		SDL_Event event;
		while ( SDL_PollEvent(&event) ){
			switch ( event.type ){
				case SDL_KEYDOWN:
					break PLAY_LOOP;
				case SDL_QUIT:
					break PLAY_LOOP;
				default:
			}
		}
	}
	// flush decoder
	if ( avcodec_send_packet(codec_context, null) != 0 ){
		writeln("avcodec_send_packet failed");
	}
	while ( avcodec_receive_frame(codec_context, frame) == 0 ){
		drawWindow(frame, window, scrWidth);
	}
}

void drawWindow(AVFrame* frame, SDL_Window* window, int scrWidth)
{
	ubyte* buffer = decodeFrame(frame, scrWidth);
	
	int w0 = (scrWidth + 7) / 8 * 8;
	SDL_Surface* image = SDL_CreateRGBSurfaceWithFormatFrom(
		cast(void *)buffer, w0, frame.height, 24, w0*3, SDL_PIXELFORMAT_BGR24);
	
	SDL_Surface* screen = SDL_GetWindowSurface(window);
	SDL_BlitSurface(image, null, screen, null);
	SDL_UpdateWindowSurface(window);
	
	SDL_FreeSurface(image);
	
	av_free(buffer);
}

ubyte* decodeFrame(AVFrame* frame, int scrWidth)
{
	int w0 = (scrWidth + 7) / 8 * 8;
	ulong size = avpicture_get_size(AVPixelFormat.AV_PIX_FMT_BGR24, w0, frame.height);
	ubyte* buffer = cast(ubyte*)av_malloc(size);
	
	AVFrame* pFrameRGB = av_frame_alloc();
	avpicture_fill(cast(AVPicture*)pFrameRGB, buffer, AVPixelFormat.AV_PIX_FMT_BGR24, w0, frame.height);
	SwsContext* pSWSCtx = sws_getContext(
		frame.width, frame.height, cast(AVPixelFormat)frame.format,
		   scrWidth, frame.height, AVPixelFormat.AV_PIX_FMT_BGR24,
		SWS_BICUBIC, null, null, null);
	sws_scale(pSWSCtx, cast(ubyte**)frame.data, cast(int*)frame.linesize, 0,
		frame.height, cast(ubyte**)pFrameRGB.data, cast(int*)pFrameRGB.linesize);
	sws_freeContext(pSWSCtx);
	av_free(pFrameRGB);
	
	return ( buffer );
}
```