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 }