1 /** 2 Module implements utilities for video input. 3 4 Input video streaming is performed with InputStream utility by following example: 5 ---- 6 InputStream stream = new InputStream; 7 8 stream.open(pathToVideoFile, InputStreamType.FILE); 9 10 if (!stream.isOpen) { 11 exit(-1); 12 } 13 14 Image frame; 15 16 while(stream.readFrame(frame)) { 17 // do something with frame... 18 } 19 ---- 20 21 Copyright: Copyright Relja Ljubobratovic 2016. 22 23 Authors: Relja Ljubobratovic 24 25 License: $(LINK3 http://www.boost.org/LICENSE_1_0.txt, Boost Software License - Version 1.0). 26 */ 27 28 module dcv.io.video.input; 29 30 import std.exception : enforce; 31 import std..string; 32 33 debug 34 { 35 import std.stdio; 36 } 37 38 import ffmpeg.libavcodec.avcodec; 39 import ffmpeg.libavformat.avformat; 40 import ffmpeg.libavutil.avutil; 41 import ffmpeg.libavutil.frame; 42 import ffmpeg.libavutil.dict; 43 import ffmpeg.libavutil.opt; 44 import ffmpeg.libavutil.mem; 45 import ffmpeg.libswscale.swscale; 46 import ffmpeg.libavdevice.avdevice; 47 import ffmpeg.libavfilter.avfilter; 48 49 public import dcv.io.video.common; 50 public import dcv.io.image; 51 52 /** 53 Input streaming type - file or webcam (live) 54 */ 55 enum InputStreamType 56 { 57 INVALID, /// Invalid stream, as non-assigned. 58 FILE, /// File video stream. 59 LIVE /// Live video stream. 60 } 61 62 /** 63 Exception thrown when seeking a frame fails. 64 */ 65 class SeekFrameException : Exception 66 { 67 @safe this(size_t frame, string file = __FILE__, size_t line = __LINE__, Throwable next = null) 68 { 69 import std.conv : to; 70 71 super("Internal error occurred while seeking frame: " ~ frame.to!string, file, line, next); 72 } 73 } 74 75 /** 76 Exception thrown when seeking a time fails. 77 */ 78 class SeekTimeException : Exception 79 { 80 @safe this(double time, string file = __FILE__, size_t line = __LINE__, Throwable next = null) 81 { 82 import std.conv : to; 83 84 super("Internal error occurred while seeking time: " ~ time.to!string, file, line, next); 85 } 86 } 87 88 /** 89 Video streaming utility. 90 */ 91 class InputStream 92 { 93 private: 94 AVFormatContext* formatContext = null; 95 AVStream* stream = null; 96 InputStreamType type = InputStreamType.INVALID; 97 98 public: 99 this() 100 { 101 AVStarter AV_STARTER_INSTANCE = AVStarter.instance(); 102 } 103 104 ~this() 105 { 106 close(); 107 } 108 109 @property const 110 { 111 private auto checkStream() 112 { 113 if (stream is null) 114 throw new StreamNotOpenException; 115 } 116 117 /// Check if stream is open. 118 auto isOpen() 119 { 120 return formatContext !is null; 121 } 122 /// Check if this stream is the file stream. 123 auto isFileStream() 124 { 125 return (type == InputStreamType.FILE); 126 } 127 /// Check if this stream is the live stream. 128 auto isLiveStream() 129 { 130 return (type == InputStreamType.LIVE); 131 } 132 133 /// Get width of the video frame. 134 auto width() 135 { 136 checkStream(); 137 return stream.codecpar.width; 138 } 139 /// Get height of the video frame. 140 auto height() 141 { 142 checkStream(); 143 return stream.codecpar.height; 144 } 145 146 /// Get size of frame in bytes. 147 auto frameSize() 148 { 149 return avpicture_get_size(pixelFormat, cast(int)width, cast(int)height); 150 } 151 152 /// Get number of frames in video. 153 auto frameCount() 154 { 155 checkStream(); 156 long fc = stream.nb_frames; 157 if (fc <= 0) 158 { 159 fc = stream.nb_index_entries; 160 } 161 return fc; 162 } 163 164 /// Get the index of the stream - most commonly is 0, where audio stream is 1. 165 auto streamIndex() 166 { 167 checkStream(); 168 return stream.index; 169 } 170 /// Get frame rate of the stream. 171 auto frameRate() 172 { 173 checkStream(); 174 double fps = stream.r_frame_rate.av_q2d; 175 if (fps < float.epsilon) 176 { 177 fps = stream.avg_frame_rate.av_q2d; 178 } 179 if (fps < float.epsilon) 180 { 181 fps = 1. / stream.codec.time_base.av_q2d; 182 } 183 184 return fps; 185 } 186 187 auto duration() 188 { 189 import std.algorithm.comparison : max; 190 191 checkStream(); 192 return stream.duration >= 0 ? stream.duration : 0; 193 } 194 } 195 196 void dumpFormat() const 197 { 198 if (!isOpen) 199 throw new StreamNotOpenException; 200 av_dump_format(cast(AVFormatContext*)formatContext, 0, "", 0); 201 } 202 203 /** 204 Open the video stream. 205 206 params: 207 path = Path to the stream. 208 type = Stream type. 209 210 return: 211 Stream opening status - true if succeeds, false otherwise. 212 213 throws: 214 StreamNotOpenException 215 */ 216 bool open(in string path, InputStreamType type = InputStreamType.FILE) 217 { 218 enforce(type != InputStreamType.INVALID, "Input stream type cannot be defined as invalid."); 219 220 this.type = type; 221 222 AVInputFormat* fmt = null; 223 224 if (isLiveStream) 225 { 226 version (Windows) 227 { 228 fmt = av_find_input_format("dshow"); 229 if (fmt is null) 230 { 231 fmt = av_find_input_format("vfwcap"); 232 } 233 } 234 else version (linux) 235 { 236 fmt = av_find_input_format("v4l2"); 237 } 238 else version (OSX) 239 { 240 fmt = av_find_input_format("avfoundation"); 241 } 242 else 243 { 244 static assert(0, "Not supported platform"); 245 } 246 if (fmt is null) 247 { 248 throw new StreamNotOpenException("Cannot find corresponding file live format for the platform"); 249 } 250 } 251 252 return openInputStreamImpl(fmt, path); 253 } 254 255 /// Close the video stream. 256 void close() 257 { 258 if (formatContext) 259 { 260 if (stream && stream.codec) 261 { 262 avcodec_close(stream.codec); 263 stream = null; 264 } 265 avformat_close_input(&formatContext); 266 formatContext = null; 267 } 268 } 269 270 /// Seek the video timeline to given frame index. 271 void seekFrame(size_t frame) 272 { 273 enforce(isFileStream, "Only input file streams can be seeked."); 274 275 if (stream is null) 276 throw new Exception("Stream is not open"); 277 278 if (!(frame < frameCount)) 279 { 280 throw new SeekFrameException(frame); 281 } 282 283 double frameDuration = 1. / frameRate; 284 double seekSeconds = frame * frameDuration; 285 int seekTarget = cast(int)(seekSeconds * (stream.time_base.den)) / (stream.time_base.num); 286 287 if (av_seek_frame(formatContext, cast(int)streamIndex, seekTarget, AVSEEK_FLAG_ANY) < 0) 288 { 289 throw new SeekFrameException(frame); 290 } 291 } 292 293 /// Seek the video timeline to given time. 294 void seekTime(double time) 295 { 296 enforce(isFileStream, "Only input file streams can be seeked."); 297 298 if (stream is null) 299 throw new StreamNotOpenException; 300 301 int seekTarget = cast(int)(time * (stream.time_base.den)) / (stream.time_base.num); 302 303 if (av_seek_frame(formatContext, cast(int)streamIndex, seekTarget, AVSEEK_FLAG_ANY) < 0) 304 { 305 throw new SeekTimeException(time); 306 } 307 } 308 309 /** 310 Read the next framw. 311 312 params: 313 image = Image where next video frame will be stored. 314 */ 315 bool readFrame(ref Image image) 316 { 317 if (isOpen) 318 { 319 return readFrameImpl(image); 320 } 321 else 322 { 323 throw new StreamNotOpenException; 324 } 325 } 326 327 private: 328 329 bool readFrameImpl(ref Image image) 330 { 331 bool stat = false; 332 333 AVPacket packet; 334 av_init_packet(&packet); 335 336 // allocating an AVFrame 337 AVFrame* frame = av_frame_alloc(); 338 if (!frame) 339 { 340 throw new Exception("Could not allocate frame."); 341 } 342 343 scope (exit) 344 { 345 av_frame_free(&frame); 346 av_free_packet(&packet); 347 } 348 349 while (av_read_frame(formatContext, &packet) >= 0) 350 { 351 int ret = 0; 352 int gotFrame = 0; 353 if (packet.stream_index == streamIndex) 354 { 355 while (true) 356 { 357 ret = avcodec_decode_video2(stream.codec, frame, &gotFrame, &packet); 358 if (ret < 0) 359 { 360 throw new StreamException("Error decoding video frame."); 361 } 362 if (gotFrame) 363 break; 364 } 365 if (gotFrame) 366 { 367 stat = true; 368 369 if (image is null || image.byteSize != frameSize) 370 { 371 image = new Image(width, height, AVPixelFormat_to_ImageFormat(pixelFormat), BitDepth.BD_8); 372 } 373 374 adoptFormat(pixelFormat, frame, image.data); 375 break; 376 } 377 } 378 } 379 return stat; 380 } 381 382 bool openInputStreamImpl(AVInputFormat* inputFormat, in string filepath) 383 { 384 const char* file = toStringz(filepath); 385 int streamIndex = -1; 386 387 // open file, and allocate format context 388 if (avformat_open_input(&formatContext, file, inputFormat, null) < 0) 389 { 390 debug writeln("Could not open stream for file: " ~ filepath); 391 return false; 392 } 393 394 // retrieve stream information 395 if (avformat_find_stream_info(formatContext, null) < 0) 396 { 397 debug writeln("Could not find stream information"); 398 return false; 399 } 400 401 if (openCodecContext(&streamIndex, formatContext, AVMediaType.AVMEDIA_TYPE_VIDEO) >= 0) 402 { 403 stream = formatContext.streams[streamIndex]; 404 } 405 406 if (!stream) 407 { 408 debug writeln("Could not find video stream."); 409 return false; 410 } 411 412 return true; 413 } 414 415 int openCodecContext(int* stream_idx, AVFormatContext* fmt_ctx, AVMediaType type) 416 { 417 int ret; 418 419 AVStream* st; 420 AVCodecContext* dec_ctx = null; 421 AVCodec* dec = null; 422 AVDictionary* opts = null; 423 424 ret = av_find_best_stream(fmt_ctx, type, -1, -1, null, 0); 425 if (ret < 0) 426 { 427 debug writeln("Could not find stream in FILE file."); 428 return ret; 429 } 430 else 431 { 432 *stream_idx = ret; 433 st = fmt_ctx.streams[*stream_idx]; 434 /* find decoder for the stream */ 435 dec_ctx = st.codec; 436 dec = avcodec_find_decoder(dec_ctx.codec_id); 437 438 if (!dec) 439 { 440 debug writeln("Failed to find codec: ", av_get_media_type_string(type)); 441 return -1; 442 } 443 444 if ((ret = avcodec_open2(dec_ctx, dec, &opts)) < 0) 445 { 446 debug writeln("Failed to open codec: ", av_get_media_type_string(type)); 447 return ret; 448 } 449 if ((ret = avcodec_parameters_to_context(dec_ctx, st.codecpar)) < 0) 450 { 451 debug writeln("Failed to get codec parameters: ", av_get_media_type_string(type)); 452 return ret; 453 } 454 } 455 456 return 0; 457 } 458 459 @property AVPixelFormat pixelFormat() const 460 { 461 if (stream is null) 462 throw new StreamNotOpenException; 463 return convertDepricatedPixelFormat(stream.codec.pix_fmt); 464 } 465 466 }