1 /**
2 Module implements utilities for video output.
3 
4 Video output streaming is performed using OutputStream utility by following example:
5 
6 ----
7 
8 Image []frames; // initialized elsewhere 
9 
10 OutputStream outputStream = new OutputStream;  // define the output video outputStream.
11 
12 OutputDefinition props;
13 
14 props.width = width;
15 props.height = height;
16 props.imageFormat = ImageFormat.IF_RGB;
17 props.bitRate = 90_000;
18 props.codecId = CodecID.H263;
19 
20 outputStream.open(filePath, props);
21 
22 if (!outputStream.isOpen) {
23     exit(-1);
24 }
25 
26 foreach(frame; frames) {
27     outputStream.writeFrame(frame);
28 }
29 
30 outputStream.close();
31 ----
32 
33 Copyright: Copyright Relja Ljubobratovic 2016.
34 
35 Authors: Relja Ljubobratovic
36 
37 License: $(LINK3 http://www.boost.org/LICENSE_1_0.txt, Boost Software License - Version 1.0).
38 */
39 
40 module dcv.io.video.output;
41 
42 debug
43 {
44     import std.stdio;
45 }
46 
47 import std.exception : enforce;
48 import std..string;
49 
50 import ffmpeg.libavcodec.avcodec;
51 import ffmpeg.libavformat.avformat;
52 import ffmpeg.libavformat.avio;
53 import ffmpeg.libavutil.avutil;
54 import ffmpeg.libavutil.opt;
55 import ffmpeg.libavutil.mem;
56 import ffmpeg.libavutil.frame;
57 import ffmpeg.libswscale.swscale;
58 import ffmpeg.libavdevice.avdevice;
59 import ffmpeg.libavfilter.avfilter;
60 
61 public import dcv.io.video.common;
62 public import dcv.io.image;
63 
64 /**
65 Output stream definition properties.
66 */
67 struct OutputDefinition
68 {
69     size_t width = 0; /// Width of the output video frame.
70     size_t height = 0; /// Height of the output video frame.
71     size_t bitRate = 400000; /// Bit rate of the output video stream.
72     size_t frameRate = 30; /// Frame rate of the output video stream.
73     ImageFormat imageFormat = ImageFormat.IF_RGB; /// Image format for video frame.
74     CodecID codecId = CodecID.NONE; /// Video codec for output video stream.
75 
76     // book-keeping parameters for video writing.
77     size_t frames = 0;
78     size_t pts = 0;
79 
80 }
81 
82 /**
83 Video stream utility used to output video content to file system.
84 */
85 class OutputStream
86 {
87 private:
88     AVFormatContext* formatContext;
89     AVStream* stream;
90     AVFrame* frame;
91     SwsContext* swsContext;
92     OutputDefinition properties;
93 
94 public:
95     /// Default initialization.
96     this()
97     {
98         AVStarter AV_STARTER_INSTANCE = AVStarter.instance();
99     }
100     /// Destructor of the stream - closes the stream.
101     ~this()
102     {
103         close();
104     }
105 
106     /// Check if stream is open.
107     @property isOpen() const
108     {
109         return formatContext !is null;
110     }
111 
112     /**
113     Open the video stream.
114 
115     Params:
116         filepath    = Path to the stream.
117         props       = Properties of the video.
118     */
119     bool open(in string filepath, in OutputDefinition props = OutputDefinition())
120     {
121         this.properties = props;
122         const char* path = toStringz(filepath);
123         char* formatString = null;
124 
125         // Determinate output format
126         AVOutputFormat* outputFormat = null;
127         if (cast(int)props.codecId)
128         {
129             formatString = (getCodecString(props.codecId).dup ~ '\0').ptr;
130             outputFormat = av_guess_format(formatString, null, null);
131         }
132         else
133         {
134             outputFormat = av_guess_format(null, path, null);
135         }
136 
137         if (outputFormat is null)
138         {
139             debug writeln("Could not find suitable output format");
140             return false;
141         }
142 
143         // Allocate format context
144         formatContext = avformat_alloc_context();
145         if (formatContext is null)
146         {
147             debug writeln("Cannot allocate context");
148             return false;
149         }
150 
151         // Open the file
152         formatContext.oformat = outputFormat;
153         formatContext.filename[0 .. filepath.length] = filepath[];
154 
155         // Find right encoder
156         auto codecCheck = AVCodecIDToCodecID(outputFormat.video_codec);
157         if (codecCheck == CodecID.NONE)
158         {
159             debug writeln("Codec is unsupported for given video format.");
160             return false;
161         }
162 
163         if (properties.codecId == CodecID.NONE)
164         {
165             properties.codecId = codecCheck;
166         }
167 
168         AVCodec* codec = avcodec_find_encoder(outputFormat.video_codec);
169         if (!codec)
170         {
171             codec = avcodec_find_encoder_by_name(formatString);
172             if (!codec)
173             {
174                 debug writeln("Cannot find encoder.");
175                 return false;
176             }
177         }
178 
179         // Add a video stream
180         stream = avformat_new_stream(formatContext, null);
181         if (stream is null)
182         {
183             debug writeln("Could not allocate stream");
184             return false;
185         }
186 
187         AVCodecContext* c = avcodec_alloc_context3(codec);
188         stream.codec = c;
189 
190         c.codec_id = outputFormat.video_codec;
191         c.codec_type = AVMediaType.AVMEDIA_TYPE_VIDEO;
192         c.bit_rate = cast(int)properties.bitRate;
193         c.width = cast(int)properties.width;
194         c.height = cast(int)properties.height;
195         c.time_base = AVRational(1, cast(int)properties.frameRate);
196         c.gop_size = 12;
197         c.pix_fmt = codec.pix_fmts[0];
198         c.frame_number = 0;
199         stream.time_base = c.time_base;
200 
201         assert(c.pix_fmt != AVPixelFormat.AV_PIX_FMT_NONE, "Codec pixel format not defined");
202 
203         if (c.codec_id == AVCodecID.AV_CODEC_ID_MPEG1VIDEO)
204         {
205             c.mb_decision = 2;
206         }
207         else if (c.codec_id == AVCodecID.AV_CODEC_ID_MPEG2VIDEO)
208         {
209             c.max_b_frames = 2;
210         }
211 
212         if (formatContext.oformat.flags & AVFMT_GLOBALHEADER)
213             c.flags |= CODEC_FLAG_GLOBAL_HEADER;
214 
215         if (properties.codecId == CodecID.H264)
216             av_opt_set(c.priv_data, "preset", "slow", 0);
217 
218         if (avcodec_open2(c, codec, null) < 0)
219         {
220             debug writeln("could not open codec");
221             return false;
222         }
223 
224         swsContext = sws_getContext(cast(int)width, cast(int)height,
225                 ImageFormat_to_AVPixelFormat(properties.imageFormat), cast(int)width,
226                 cast(int)height, c.pix_fmt, SWS_BICUBIC, null, null, null);
227 
228         // Allocate output frame
229         frame = allocPicture(c.pix_fmt, c.width, c.height);
230         if (!frame)
231         {
232             debug writeln("Could not allocate frame\n");
233             return false;
234         }
235 
236         // open file
237         if (avio_open(&formatContext.pb, path, AVIO_FLAG_WRITE) < 0)
238         {
239             debug writeln("Cannot open file at given path");
240             return false;
241         }
242 
243         // Write stream header, if any
244         avformat_write_header(formatContext, null);
245 
246         return true;
247     }
248 
249     /// Close the output stream.
250     void close()
251     {
252         if (formatContext)
253         {
254             av_write_trailer(formatContext);
255             avio_close(formatContext.pb);
256             formatContext = null;
257         }
258         if (stream)
259         {
260             avcodec_close(stream.codec);
261             av_freep(stream);
262             stream = null;
263         }
264         if (frame)
265         {
266             av_frame_free(&frame);
267             frame = null;
268         }
269         if (swsContext)
270         {
271             sws_freeContext(swsContext);
272             swsContext = null;
273         }
274     }
275 
276     /// Write given image as new frame of the image.
277     bool writeFrame(Image image)
278     {
279         import ffmpeg.libavutil.mathematics;
280 
281         enforce(image.format == properties.imageFormat, "Image format does not match the output configuration.");
282         enforce(image.height == properties.height, "Image height does not match the output configuration.");
283         enforce(image.width == properties.width, "Image width does not match the output configuration.");
284 
285         AVPacket packet;
286         av_init_packet(&packet);
287         packet.data = null;
288         packet.size = 0;
289 
290         scope (exit)
291         {
292             av_packet_unref(&packet);
293             av_free_packet(&packet);
294         }
295 
296         ubyte*[] data;
297         int[] linesize;
298 
299         extractDataFromImage(image, data, linesize);
300 
301         sws_scale(swsContext, data.ptr, linesize.ptr, 0, cast(int)height, frame.data.ptr, frame.linesize.ptr);
302 
303         int gotPacket = 0;
304 
305         if (formatContext.oformat.flags & AVFMT_RAWPICTURE)
306         {
307 
308             packet.flags |= AV_PKT_FLAG_KEY;
309             packet.stream_index = stream.index;
310             packet.data = cast(ubyte*)frame;
311             packet.size = frame.sizeof;
312 
313             gotPacket = 1;
314 
315         }
316         else
317         {
318             while (true)
319             {
320                 int outSize = avcodec_encode_video2(stream.codec, &packet, frame, &gotPacket);
321                 frame.pts++;
322                 if (gotPacket)
323                 {
324                     break;
325                 }
326             }
327         }
328         if (!gotPacket)
329             return false;
330 
331         if (packet.pts != AV_NOPTS_VALUE)
332             packet.pts = av_rescale_q(packet.pts, stream.codec.time_base, stream.time_base);
333 
334         if (packet.dts != AV_NOPTS_VALUE)
335             packet.dts = av_rescale_q(packet.dts, stream.codec.time_base, stream.time_base);
336 
337         if (stream.codec.coded_frame.key_frame)
338             packet.flags |= AV_PKT_FLAG_KEY;
339 
340         auto ret = av_interleaved_write_frame(formatContext, &packet);
341 
342         if (ret != 0)
343         {
344             debug writeln("Error writing frame");
345             return false;
346         }
347         else
348         {
349             return true;
350         }
351     }
352 
353     @property const
354     {
355         /// Width of the frame image.
356         auto width()
357         {
358             return properties.width;
359         }
360         /// Height of the frame image.
361         auto height()
362         {
363             return properties.height;
364         }
365         /// Current frame count of the output stream.
366         auto frameCount()
367         {
368             return properties.frames;
369         }
370         /// Frame rate of the stream.
371         auto frameRate()
372         {
373             return properties.frameRate;
374         }
375         /// Codec of the stream.
376         auto codec()
377         {
378             return properties.codecId;
379         }
380     }
381 
382 private:
383 
384     AVFrame* allocPicture(AVPixelFormat pix_fmt, int width, int height)
385     {
386         AVFrame* picture;
387         ubyte[] picture_buf;
388         int size;
389 
390         picture = av_frame_alloc();
391         if (!picture)
392         {
393             debug writeln("Cannot allocate memory for the frame");
394             return null;
395         }
396 
397         size = avpicture_get_size(pix_fmt, width, height);
398         picture_buf.length = size;
399         avpicture_fill(cast(AVPicture*)picture, picture_buf.ptr, pix_fmt, width, height);
400 
401         picture.format = pix_fmt;
402         picture.width = width;
403         picture.height = height;
404         picture.pts = 0;
405 
406         return picture;
407     }
408 
409     @property AVPixelFormat pixelFormat() const
410     {
411         return convertDepricatedPixelFormat(stream.codec.pix_fmt);
412     }
413 }
414 
415 private:
416 
417 CodecID AVCodecIDToCodecID(AVCodecID avcodecId)
418 {
419     CodecID c;
420 
421     switch (avcodecId)
422     {
423     case AVCodecID.AV_CODEC_ID_RAWVIDEO, AVCodecID.AV_CODEC_ID_MPEG1VIDEO, AVCodecID.AV_CODEC_ID_MPEG2VIDEO,
424             AVCodecID.AV_CODEC_ID_MPEG4, AVCodecID.AV_CODEC_ID_H263, AVCodecID.AV_CODEC_ID_H264:
425             c = cast(CodecID)(cast(int)avcodecId);
426         break;
427     default:
428         c = CodecID.NONE;
429     }
430     return c;
431 }
432 
433 void extractDataFromImage(in Image image, ref ubyte*[] data, ref int[] linesize)
434 {
435     enforce(image.depth == BitDepth.BD_8, "Image bit depth not supported so far."); // todo: support other types
436 
437     auto pixelCount = image.width * image.height;
438     auto imdata = image.data;
439     auto w = cast(int)image.width;
440     auto h = cast(int)image.height;
441 
442     switch (image.format)
443     {
444     case ImageFormat.IF_MONO:
445         data = [image.data.ptr];
446         linesize = [w];
447         break;
448     case ImageFormat.IF_MONO_ALPHA:
449         data = [image.data.ptr];
450         linesize = [w * 2];
451         break;
452     case ImageFormat.IF_RGB, ImageFormat.IF_BGR:
453         data = [image.data.ptr];
454         linesize = [w * 3];
455         break;
456     case ImageFormat.IF_RGB_ALPHA, ImageFormat.IF_BGR_ALPHA:
457         data = [image.data.ptr];
458         linesize = [w * 4];
459         break;
460     case ImageFormat.IF_YUV:
461         data = [new ubyte[w * h].ptr, new ubyte[w * h].ptr, new ubyte[w * h].ptr];
462         foreach (i; 0 .. pixelCount)
463         {
464             data[0][i] = imdata[i * 3 + 0];
465             data[0][i] = imdata[i * 3 + 1];
466             data[0][i] = imdata[i * 3 + 2];
467         }
468         linesize = [w, w, w];
469         break;
470     default:
471         assert(0);
472     }
473 }