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 }