(*
tutorial04.c
A pedagogical video player that will stream through every video frame as fast as it can
and play audio (out of sync).
This tutorial was written by Stephen Dranger (dranger@gmail.com).
Code based on FFplay, Copyright (c) 2003 Fabrice Bellard,
and a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
Conversion to Delphi by Oleksandr Nazaruk (mail@freehand.com.ua)
Tested on Windows 8.1 64bit rus, compiled with Delphi XE5
Run using
tutorial04 myvideofile.mpg
to play the video stream on your screen.
*)
program tutorial04;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils,
System.Classes,
math,
SDL2 in '../../../Injest/LIB/SDL/Pascal-SDL-2-Header/SDL2.pas',
avcodec in '../FFmbc-0.7/libavcodec/avcodec.pas',
avformat in '../FFmbc-0.7/libavformat/avformat.pas',
avio in '../FFmbc-0.7/libavformat/avio.pas',
avutil in '../FFmbc-0.7/libavutil/avutil.pas',
opt in '../FFmbc-0.7/libavutil/opt.pas',
rational in '../FFmbc-0.7/libavutil/rational.pas',
imgutils in '../FFmbc-0.7/libavutil/imgutils.pas',
fifo in '../FFmbc-0.7/libavutil/fifo.pas',
file_ in '../FFmbc-0.7/libavutil/file_.pas',
ctypes in '../FFmbc-0.7/ctypes.pas',
swscale in '../FFmbc-0.7/libswscale/swscale.pas',
avdevice in '../FFmbc-0.7/libavdevice/avdevice.pas',
postprocess in '../FFmbc-0.7/libpostproc/postprocess.pas';
const
SDL_AUDIO_BUFFER_SIZE = 1024;
MAX_AUDIO_FRAME_SIZE = 192000;
MAX_AUDIOQ_SIZE = (5 * 16 * 1024);
MAX_VIDEOQ_SIZE = (5 * 256 * 1024);
USE_AUDIO_DRIVER = 'directsound';
FF_ALLOC_EVENT = (SDL_USEREVENT);
FF_REFRESH_EVENT = (SDL_USEREVENT + 1);
FF_QUIT_EVENT = (SDL_USEREVENT + 2);
VIDEO_PICTURE_QUEUE_SIZE = 1;
type
PPacketQueue =^TPacketQueue;
TPacketQueue = record
first_pkt : PAVPacketList;
last_pkt : PAVPacketList;
nb_packets : integer;
size : integer;
mutex : PSDL_mutex;
cond : PSDL_cond;
end;
PVideoPicture = ^TVideoPicture;
TVideoPicture = record
bmp : PSDL_texture;
width, height : integer; (* source height & width *)
allocated : integer;
buffer : pcuint8;
pFrameYUV420P : PAVFrame;
end;
PVideoState = ^TVideoState;
TVideoState = record
pFormatCtx : PAVFormatContext;
videoStream : integer;
audioStream : integer;
audio_st : PAVStream;
audioq : PPacketQueue;
audio_buf : array[0..((AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) div 2)] of cuint8;
audio_buf_size : cardinal;
audio_buf_index : cardinal;
audio_frame : TAVFrame;
audio_pkt : TAVPacket;
audio_pkt_data : pcuint8;
audio_pkt_size : integer;
video_st : PAVStream;
videoq : PPacketQueue;
pictq : array[0..VIDEO_PICTURE_QUEUE_SIZE] of TVideoPicture;
pictq_size : integer;
pictq_rindex : integer;
pictq_windex : integer;
pictq_mutex : PSDL_mutex;
pictq_cond : PSDL_cond;
parse_tid : PSDL_Thread;
video_tid : PSDL_Thread;
filename : array[0..1024] of ansichar;
quit : boolean;
io_context : PAVIOContext;
sws_ctx : PSwsContext;
end;
var
screen : PSDL_Window = nil;
render : PSDL_renderer = nil;
(* Since we only have one decoding thread, the Big Struct
can be global in case we need it. *)
global_video_state : PVideoState;
procedure packet_queue_init(var q: PPacketQueue);
begin
if not assigned(q) then
new(q);
fillchar(q^, sizeof(TPacketQueue), #0);
q.mutex := SDL_CreateMutex();
q.cond := SDL_CreateCond();
end;
function packet_queue_put(var q: PPacketQueue; pkt: PAVPacket): integer;
var
pkt1 : PAVPacketList;
begin
result:=-1;
if not assigned(pkt) then
exit;
if(av_dup_packet(pkt) < 0) then
begin
result:=-1;
exit;
end;
pkt1 := av_malloc(sizeof(TAVPacketList));
if not assigned(pkt1) then
begin
result:=-1;
exit;
end;
pkt1^.pkt := pkt^;
pkt1^.next := nil;
SDL_LockMutex(q.mutex);
try
if not assigned(q.last_pkt) then
q.first_pkt := pkt1
else
q.last_pkt^.next := pkt1;
q.last_pkt := pkt1;
inc(q.nb_packets);
q.size:=q.size+pkt1.pkt.size;
SDL_CondSignal(q.cond);
finally
SDL_UnlockMutex(q.mutex);
end;
result:=0;
end;
function packet_queue_get(var q: PPacketQueue; var pkt: TAVPacket; block: integer): integer;
var
pkt1 : PAVPacketList;
ret : integer;
begin
result:=-1;
ret:=-1;
SDL_LockMutex(q.mutex);
try
while true do
begin
if global_video_state.quit then
begin
result:=-1;
break;
end;
pkt1 := q.first_pkt;
if Assigned(pkt1) then
begin
q.first_pkt := pkt1^.next;
if not assigned(q.first_pkt) then
q.last_pkt := nil;
dec(q.nb_packets);
q.size := q.size-pkt1^.pkt.size;
pkt := pkt1^.pkt;
av_free(pkt1);
ret := 1;
break;
end else
if (block)<=0 then
begin
ret := 0;
break;
end else begin
SDL_CondWait(q.cond, q.mutex);
end;
end;
finally
SDL_UnlockMutex(q.mutex);
end;
result:=ret;
end;
function audio_decode_frame(is_: PVideoState): integer;
var
pkt : TAVPacket; /////////// PAVPacket
len1 : integer;
data_size : integer;
buffer_size : integer;
begin
data_size:=0;
len1:=0;
pkt:=is_.audio_pkt;
while True do
begin
while (is_.audio_pkt_size > 0) do
begin
buffer_size := ((AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) div 2);
len1 := avcodec_decode_audio3(is_.audio_st.codec, PSmallInt(@is_.audio_buf[0]), buffer_size, @pkt);
if(len1 <= 0) then
begin
(* if error, skip frame *)
is_.audio_pkt_size := 0;
break;
end;
inc(is_.audio_pkt_data,len1);
dec(is_.audio_pkt_size,len1);
inc(data_size, len1);
if(data_size <= 0) then
begin
(* No data yet, get more frames *)
continue;
end;
(* We have data, return it and come back for more later *)
result:=data_size;
exit;
end;
if assigned(pkt.data) then
av_free_packet(@pkt);
if is_.quit then
begin
result:=-1;
exit;
end;
if(packet_queue_get(is_.audioq, pkt, 1) < 0) then
begin
result:=-1;
exit;
end;
is_.audio_pkt_data := pkt.data;
is_.audio_pkt_size := pkt.size;
end;
end;
procedure audio_callback(userdata: Pointer; stream: PUInt8; len: LongInt); cdecl;
var
is_ : PVideoState;
audio_size : integer;
len1 : integer;
begin
is_:=PVideoState(userdata);
while (len > 0) do
begin
if(is_.audio_buf_index >= is_.audio_buf_size) then
begin
(* We have already sent all our data; get more *)
audio_size := audio_decode_frame(is_);
if(audio_size < 0) then
begin
(* If error, output silence *)
is_.audio_buf_size := 1024; // arbitrary?
fillchar(is_.audio_buf[0], is_.audio_buf_size, #0);
end else begin
is_.audio_buf_size := audio_size;
end;
is_.audio_buf_index := 0;
end;
len1 := is_.audio_buf_size - is_.audio_buf_index;
if(len1 > len) then
len1 := len;
move(is_.audio_buf[is_.audio_buf_index], stream^, len1);
dec(len, len1);
inc(stream,len1);
inc(is_.audio_buf_index,len1);
end;
end;
function sdl_refresh_timer_cb(interval: cardinal; opaque : Pointer): cardinal; cdecl;
var
event : TSDL_Event;
begin
event.type_ := FF_REFRESH_EVENT;
event.user.data1 := opaque;
SDL_PushEvent(@event);
result:=0; (* 0 means stop timer *)
end;
(* schedule a video refresh in 'delay' ms *)
procedure schedule_refresh(is_: PVideoState; delay: integer);
begin
SDL_AddTimer(delay, @sdl_refresh_timer_cb, is_);
end;
procedure video_display(is_: PVideoState);
var
vp : PVideoPicture;
aspect_ratio : float;
w, h, x, y : integer;
begin
vp := @is_.pictq[is_.pictq_rindex];
if assigned(vp.bmp) then
begin
if(is_.video_st.codec.sample_aspect_ratio.num = 0) then
begin
aspect_ratio := 0;
end else
begin
aspect_ratio := av_q2d(is_.video_st.codec.sample_aspect_ratio) *
is_.video_st.codec.width / is_.video_st.codec.height;
end;
if (aspect_ratio <= 0.0) then
begin
aspect_ratio := is_.video_st.codec.width /is_.video_st.codec.height;
end;
h := screen.h;
w := (round(h * aspect_ratio)) and -3;
if(w > screen.w) then
begin
w := screen.w;
h := (round(w / aspect_ratio)) and -3;
end;
x := (screen.w - w) div 2;
y := (screen.h - h) div 2;
SDL_RenderClear(Render);
SDL_RenderCopy(Render, vp.bmp, nil, nil);
SDL_RenderPresent(Render);
end;
end;
procedure video_refresh_timer(userdata: pointer);
var
is_ : PVideoState;
begin
is_ := PVideoState(userdata);
// vp is used in later tutorials for synchronization
//VideoPicture *vp;
if assigned(is_.video_st) then
begin
if (is_.pictq_size = 0) then
begin
schedule_refresh(is_, 1)
end else
begin
//vp = &is->pictq[is->pictq_rindex];
(* Now, normally here goes a ton of code
about timing, etc. we're just going to
guess at a delay for now. You can
increase and decrease this value and hard code
the timing - but I don't suggest that ;)
We'll learn how to do it for real later.
*)
schedule_refresh(is_, 80);
(* show the picture! *)
video_display(is_);
(* update queue for next picture! *)
inc(is_.pictq_rindex);
if (is_.pictq_rindex = VIDEO_PICTURE_QUEUE_SIZE) then
begin
is_.pictq_rindex := 0;
end;
SDL_LockMutex(is_.pictq_mutex);
dec(is_.pictq_size);
SDL_CondSignal(is_.pictq_cond);
SDL_UnlockMutex(is_.pictq_mutex);
end;
end else
begin
schedule_refresh(is_, 100);
end;
end;
procedure alloc_picture(userdata: pointer);
var
is_ : PVideoState;
vp : PVideoPicture;
numBytes : integer;
begin
is_ := PVideoState(userdata);
vp := @is_.pictq[is_.pictq_windex];
if assigned(vp.bmp) then
begin
// we already have one make another, bigger/smaller
SDL_DestroyTexture(vp.bmp);
end;
if assigned(vp.pFrameYUV420P) then
begin
av_free(vp.pFrameYUV420P);
end;
if assigned(vp.buffer) then
begin
av_free(vp.buffer);
end;
// Allocate an AVFrame structure
vp.pFrameYUV420P:=avcodec_alloc_frame();
if not assigned(vp.pFrameYUV420P) then
begin
writeln('Could not Allocate AVFrame structure');
exit;
end;
// Determine required buffer size and allocate buffer
numBytes:=avpicture_get_size(is_.video_st.codec.pix_fmt, is_.video_st.codec.width, is_.video_st.codec.height);
vp.buffer:=av_malloc(numBytes*sizeof(cuint));
// Assign appropriate parts of buffer to image planes in pFrameYUV420P
// Note that pFrameRGB is an AVFrame, but AVFrame is a superset
// of AVPicture
avpicture_fill(PAVPicture(vp.pFrameYUV420P), vp.buffer, PIX_FMT_YUV420P, is_.video_st.codec.width, is_.video_st.codec.height);
// Allocate a place to put our YUV image on that screen
vp.bmp:=SDL_CreateTexture(render, SDL_PIXELFORMAT_YV12, sint32(SDL_TEXTUREACCESS_STREAMING), is_.video_st.codec.width, is_.video_st.codec.height);
vp.width := is_.video_st.codec.width;
vp.height := is_.video_st.codec.height;
SDL_LockMutex(is_.pictq_mutex);
vp.allocated := 1;
SDL_CondSignal(is_.pictq_cond);
SDL_UnlockMutex(is_.pictq_mutex);
end;
function queue_picture(is_: PVideoState; pFrame: PAVFrame): integer;
var
vp : PVideoPicture;
pict : TAVPicture;
event : TSDL_Event;
begin
(* wait until we have space for a new pic *)
SDL_LockMutex(is_.pictq_mutex);
while ((is_.pictq_size >= VIDEO_PICTURE_QUEUE_SIZE) and (not is_.quit)) do
begin
SDL_CondWait(is_.pictq_cond, is_.pictq_mutex);
end;
SDL_UnlockMutex(is_.pictq_mutex);
if is_.quit then
begin
result:=-1;
exit;
end;
// windex is set to 0 initially
vp := @is_.pictq[is_.pictq_windex];
(* allocate or resize the buffer! *)
if ((not assigned(vp.bmp)) or
(vp.width <> is_.video_st.codec.width) or
(vp.height <> is_.video_st.codec.height)) then
begin
vp.allocated := 0;
(* we have to do it in the main thread *)
event.type_ := FF_ALLOC_EVENT;
event.user.data1 := is_;
SDL_PushEvent(@event);
(* wait until we have a picture allocated *)
SDL_LockMutex(is_.pictq_mutex);
while ((vp.allocated=0) and (not is_.quit)) do
begin
SDL_CondWait(is_.pictq_cond, is_.pictq_mutex);
end;
SDL_UnlockMutex(is_.pictq_mutex);
if is_.quit then
begin
result:=-1;
exit;
end;
end;
(* We have a place to put our picture on the queue *)
if assigned(vp.bmp) then
begin
pict.data[0] := vp.pFrameYUV420P.data[0];
pict.data[1] := vp.pFrameYUV420P.data[2];
pict.data[2] := vp.pFrameYUV420P.data[1];
pict.linesize[0] := vp.pFrameYUV420P.linesize[0];
pict.linesize[1] := vp.pFrameYUV420P.linesize[2];
pict.linesize[2] := vp.pFrameYUV420P.linesize[1];
// Convert the image into YUV format that SDL uses
sws_scale
(
is_.sws_ctx,
@pFrame.data,
@pFrame.linesize,
0,
is_.video_st.codec.height,
@pict.data,
@pict.linesize
);
SDL_UpdateTexture(vp.bmp, nil, vp.buffer, is_.video_st.codec.width);
(* now we inform our display thread that we have a pic ready *)
inc(is_.pictq_windex);
if(is_.pictq_windex = VIDEO_PICTURE_QUEUE_SIZE) then
is_.pictq_windex := 0;
SDL_LockMutex(is_.pictq_mutex);
inc(is_.pictq_size);
SDL_UnlockMutex(is_.pictq_mutex);
end;
result:=0;
end;
function video_thread(arg: Pointer): integer; cdecl;
var
is_ : PVideoState;
pkt1 : TAVPacket;
packet : TAVPacket;
frameFinished : integer;
pFrame : PAVFrame;
begin
is_:= PVideoState(arg);
packet:= pkt1; //////////PAVPAcket
pFrame := avcodec_alloc_frame();
while true do
begin
if(packet_queue_get(is_.videoq, packet, 1) < 0) then
// means we quit getting packets
break;
// Decode video frame
avcodec_decode_video2(is_.video_st.codec, pFrame, frameFinished, @packet);
// Did we get a video frame?
if (frameFinished)>0 then
begin
if(queue_picture(is_, pFrame) < 0) then break;
end;
av_free_packet(@packet);
end;
av_free(pFrame);
result:=0;
end;
function stream_component_open(var is_: PVideoState; stream_index: integer): integer;
var
pFormatCtx : PAVFormatContext;
codecCtx : PAVCodecContext;
codec : PAVCodec;
optionsDict : PAVDictionary;
wanted_spec : TSDL_AudioSpec;
spec : TSDL_AudioSpec;
pStreams : PPAVStream;
pStream : PAVStream;
i : integer;
begin
result:=-1;
pStream := nil;
pFormatCtx := is_.pFormatCtx;
codecCtx := nil;
codec := nil;
optionsDict:= nil;
if((stream_index < 0) or (stream_index >= pFormatCtx.nb_streams)) then
begin
result:=-1;
exit;
end;
// Get a pointer to the codec context for the video stream
pStreams:=pFormatCtx.streams;
for i:=0 to pFormatCtx.nb_streams-1 do
begin
if i=stream_index then
begin
pStream:=pStreams^;
codecCtx:=pStream.codec;
end;
inc(pStreams);
end;
if(codecCtx^.codec_type = AVMEDIA_TYPE_AUDIO) then
begin
// Set audio settings from codec info
wanted_spec.freq := codecCtx^.sample_rate;
wanted_spec.format := AUDIO_S16;
wanted_spec.channels := codecCtx^.channels;
wanted_spec.silence := 0;
wanted_spec.samples := SDL_AUDIO_BUFFER_SIZE;
wanted_spec.callback := @audio_callback;
wanted_spec.userdata := is_;
if(SDL_OpenAudio(@wanted_spec, @spec) <> 0) then
begin
writeln(format('SDL_OpenAudio: %s', [SDL_GetError()]));
//exit;
end;
end;
codec := avcodec_find_decoder(codecCtx.codec_id);
if ((not assigned(codec)) or (avcodec_open2(codecCtx, codec, @optionsDict)<0)) then
begin
writeln('Unsupported codec!');
exit;
end;
case codecCtx.codec_type of
AVMEDIA_TYPE_AUDIO:
begin
is_.audioStream := stream_index;
is_.audio_st := pStream;
is_.audio_buf_size := 0;
is_.audio_buf_index := 0;
fillchar(is_.audio_pkt, sizeof(is_.audio_pkt), #0);
packet_queue_init(is_.audioq);
SDL_PauseAudio(0);
end;
AVMEDIA_TYPE_VIDEO:
begin
is_.videoStream := stream_index;
is_.video_st := pStream;
packet_queue_init(is_.videoq);
is_.video_tid := SDL_CreateThread(@video_thread, nil, is_);
is_.sws_ctx :=
sws_getContext
(
is_.video_st.codec.width,
is_.video_st.codec.height,
is_.video_st.codec.pix_fmt,
is_.video_st.codec.width,
is_.video_st.codec.height,
PIX_FMT_YUV420P,
SWS_BILINEAR,
nil,
nil,
nil
);
end;
end;
result:=0;
end;
function decode_interrupt_cb(opaque: Pointer): integer;
begin
if (assigned(global_video_state) and (global_video_state.quit)) then
result:=1 else result:=0;
end;
function decode_thread(arg: pointer): LongInt; cdecl;
label
fail;
var
is_ : PVideoState;
pFormatCtx : PAVFormatContext;
packet : PAVPacket;
pkt1 : TAVPacket;
video_index : integer;
audio_index : integer;
i : integer;
io_dict : PAVDictionary;
pStream : PPAVStream;
event : TSDL_Event;
begin
is_ := PVideoState(arg);
pFormatCtx:= nil;
packet := @pkt1;
video_index := -1;
audio_index := -1;
io_dict := nil;
is_.videoStream := -1;
is_.audioStream := -1;
global_video_state := is_;
if avio_open(is_.io_context, @is_.filename[0], 0)<0 then
begin
writeln(format('Unable to open I/O for %s', [ansistring(is_.filename)]));
result:=-1;
exit;
end;
// Open video file
if(avformat_open_input(@pFormatCtx, @is_.filename[0], nil, nil)<>0) then
begin
writeln(format('Could not open source file %s', [ansistring(is_.filename)]));
result:=-1;
exit;
end;
is_.pFormatCtx := pFormatCtx;
// Retrieve stream information
if avformat_find_stream_info(pFormatCtx , nil) < 0 then
begin
writeln(format('Could not find stream information', []));
result:=-1;
exit;
end;
// Dump information about file onto standard error
av_dump_format(pFormatCtx, 0, @is_.filename[0], 0);
// Find the first video stream
pStream:=pFormatCtx.streams;
for i:=0 to pFormatCtx.nb_streams-1 do
begin
if ((pStream^.codec.codec_type = AVMEDIA_TYPE_VIDEO) and (video_index<0)) then
begin
video_index := i;
end else
if ((pStream^.codec.codec_type = AVMEDIA_TYPE_AUDIO) and (audio_index < 0)) then
begin
audio_index := i;
end;
inc(pStream);
end;
if (audio_index >= 0) then
stream_component_open(is_, audio_index);
if(video_index >= 0) then
stream_component_open(is_, video_index);
if ((is_.videoStream < 0) or (is_.audioStream < 0)) then
begin
writeln(format('%s: could not open codecs', [ansistring(is_.filename)]));
goto fail;
end;
// main decode loop
while True do
begin
if is_.quit then
break;
// seek stuff goes here
if((is_.audioq.size > MAX_AUDIOQ_SIZE) or
(is_.videoq.size > MAX_VIDEOQ_SIZE)) then
begin
SDL_Delay(10);
continue;
end;
if(av_read_frame(is_.pFormatCtx, packet^) < 0) then
begin
if(is_.pFormatCtx.pb.error = 0) then
begin
SDL_Delay(100); (* no error; wait for user input *)
continue;
end else begin
break;
end;
end;
// Is this a packet from the video stream?
if(packet.stream_index = is_.videoStream) then
packet_queue_put(is_.videoq, packet)
else if(packet.stream_index = is_.audioStream) then
packet_queue_put(is_.audioq, packet)
else av_free_packet(@packet);
end;
(* all done - wait for it *)
while (not is_.quit) do
SDL_Delay(100);
fail:
begin
event.type_ := FF_QUIT_EVENT;
event.user.data1 := is_;
SDL_PushEvent(@event);
end;
result:=0;
end;
var
event : TSDL_event;
is_ : PVideoState;
src_filename : ansistring;
FAudioDriver : ansistring;
begin
try
new(is_);
if (ParamCount < 1) then
begin
writeln('Please provide a movie file');
exit;
end;
src_filename:=(AnsiString(ParamStr(1)));
if SDL_Init(SDL_INIT_VIDEO or SDL_INIT_AUDIO or SDL_INIT_TIMER)<0 then
begin
writeln(format('Could not initialize SDL - %s', [SDL_GetError()]));
exit;
end;
// Register all formats and codecs
av_register_all();
StrPCopy(is_.filename, src_filename);
// List audio driver
//for I := 0 to SDL_GetNumAudioDrivers-1 do
// FAudioDriver:=SDL_GetAudioDriver(i);
// Set audio driver
FAudioDriver:=USE_AUDIO_DRIVER;
SDL_AudioInit(PAnsiChar(FAudioDriver));
// Make a screen to put our video
screen:=SDL_CreateWindow('Tutorial_04', SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 720, 576, SDL_WINDOW_SHOWN );
if not assigned(screen) then
begin
writeln('SDL: could CreateWindow');
exit;
end;
render := SDL_CreateRenderer(screen, -1, 0);
is_.pictq_mutex := SDL_CreateMutex();
is_.pictq_cond := SDL_CreateCond();
schedule_refresh(is_, 40);
is_.parse_tid := SDL_CreateThread(@decode_thread, nil, is_);
if not assigned(is_.parse_tid) then
begin
av_free(is_);
exit;
end;
while True do
begin
SDL_WaitEvent(@event);
case event.type_ of
FF_QUIT_EVENT:
begin
break;
end;
SDL_QUITEV:
begin
is_.quit := true;
(*
* If the video has finished playing, then both the picture and
* audio queues are waiting for more data. Make them stop
* waiting and terminate normally.
*)
SDL_CondSignal(is_.audioq.cond);
SDL_CondSignal(is_.videoq.cond);
SDL_Quit();
break;
end;
FF_ALLOC_EVENT:
begin
alloc_picture(event.user.data1);
end;
FF_REFRESH_EVENT:
begin
video_refresh_timer(event.user.data1);
end;
end;
end;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
Stand:
- Windows 8.1 Enterprise 64Bit Rus
- Delphi XE5
Links:
- Dranger tutorial04 is ffmpeg and SDL Tutorial or How to Write a Video Player in Less Than 1000 Lines
Coin Casino: No Deposit Bonus & Free Spins › Coin › coin-casino › no-deposit-casino
ВідповістиВидалитиThe Coin Casino is a new and gioco digitale innovative online casino created in 2017. Established in 2017, this online casino is open for real players from all over the world. 코인카지노 With great odds sbobet ทางเข้า and