1 /**********************************************************************
2 
3 Audacity: A Digital Audio Editor
4 
5 ImportFFmpeg.cpp
6 
7 Copyright 2008  LRN
8 Based on ImportFLAC.cpp by Sami Liedes and transcode_sample.c by ANYwebcam Pty Ltd
9 Licensed under the GNU General Public License v2 or later
10 
11 *//****************************************************************//**
12 
13 \class FFmpegImportFileHandle
14 \brief An ImportFileHandle for FFmpeg data
15 
16 *//****************************************************************//**
17 
18 \class FFmpegImportPlugin
19 \brief An ImportPlugin for FFmpeg data
20 
21 *//*******************************************************************/
22 
23 
24 
25 // For compilers that support precompilation, includes "wx/wx.h".
26 #include <wx/wxprec.h>
27 
28 #include "../FFmpeg.h"
29 #include "FFmpegFunctions.h"
30 
31 #ifndef WX_PRECOMP
32 // Include your minimal set of headers here, or wx.h
33 #include <wx/log.h>
34 #include <wx/window.h>
35 #endif
36 
37 #include "../widgets/ProgressDialog.h"
38 
39 
40 #define DESC XO("FFmpeg-compatible files")
41 
42 //TODO: remove non-audio extensions
43 #if defined(USE_FFMPEG)
44 static const auto exts = {
45    wxT("4xm"),
46    wxT("MTV"),
47    wxT("roq"),
48    wxT("aac"),
49    wxT("ac3"),
50    wxT("aif"),
51    wxT("aiff"),
52    wxT("afc"),
53    wxT("aifc"),
54    wxT("al"),
55    wxT("amr"),
56    wxT("apc"),
57    wxT("ape"),
58    wxT("apl"),
59    wxT("mac"),
60    wxT("asf"),
61    wxT("wmv"),
62    wxT("wma"),
63    wxT("au"),
64    wxT("avi"),
65    wxT("avs"),
66    wxT("bethsoftvid"),
67    wxT("c93"),
68    wxT("302"),
69    wxT("daud"),
70    wxT("dsicin"),
71    wxT("dts"),
72    wxT("dv"),
73    wxT("dxa"),
74    wxT("ea"),
75    wxT("cdata"),
76    wxT("ffm"),
77    wxT("film_cpk"),
78    wxT("flac"),
79    wxT("flic"),
80    wxT("flv"),
81    wxT("gif"),
82    wxT("gxf"),
83    wxT("idcin"),
84    wxT("image2"),
85    wxT("image2pipe"),
86    wxT("cgi"),
87    wxT("ipmovie"),
88    wxT("nut"),
89    wxT("lmlm4"),
90    wxT("m4v"),
91    wxT("mkv"),
92    wxT("mm"),
93    wxT("mmf"),
94    wxT("mov"),
95    wxT("mp4"),
96    wxT("m4a"),
97    wxT("m4r"),
98    wxT("3gp"),
99    wxT("3g2"),
100    wxT("mj2"),
101    wxT("mp3"),
102    wxT("mpc"),
103    wxT("mpc8"),
104    wxT("mpg"),
105    wxT("mpeg"),
106    wxT("ts"),
107    wxT("mpegtsraw"),
108    wxT("mpegvideo"),
109    wxT("msnwctcp"),
110    wxT("ul"),
111    wxT("mxf"),
112    wxT("nsv"),
113    wxT("nuv"),
114    wxT("ogg"),
115    wxT("opus"),
116    wxT("psxstr"),
117    wxT("pva"),
118    wxT("redir"),
119    wxT("rl2"),
120    wxT("rm"),
121    wxT("ra"),
122    wxT("rv"),
123    wxT("rtsp"),
124    wxT("s16be"),
125    wxT("sw"),
126    wxT("s8"),
127    wxT("sb"),
128    wxT("sdp"),
129    wxT("shn"),
130    wxT("siff"),
131    wxT("vb"),
132    wxT("son"),
133    wxT("smk"),
134    wxT("sol"),
135    wxT("swf"),
136    wxT("thp"),
137    wxT("tiertexseq"),
138    wxT("tta"),
139    wxT("txd"),
140    wxT("u16be"),
141    wxT("uw"),
142    wxT("ub"),
143    wxT("u8"),
144    wxT("vfwcap"),
145    wxT("vmd"),
146    wxT("voc"),
147    wxT("wav"),
148    wxT("wc3movie"),
149    wxT("wsaud"),
150    wxT("wsvqa"),
151    wxT("wv")
152 };
153 
154 // all the includes live here by default
155 #include "Import.h"
156 #include "../Tags.h"
157 #include "../WaveTrack.h"
158 #include "ImportPlugin.h"
159 
160 class FFmpegImportFileHandle;
161 
162 /// A representative of FFmpeg loader in
163 /// the Audacity import plugin list
164 class FFmpegImportPlugin final : public ImportPlugin
165 {
166 public:
FFmpegImportPlugin()167    FFmpegImportPlugin():
168       ImportPlugin( FileExtensions( exts.begin(), exts.end() ) )
169    {
170    }
171 
~FFmpegImportPlugin()172    ~FFmpegImportPlugin() { }
173 
GetPluginStringID()174    wxString GetPluginStringID() override { return wxT("libav"); }
175    TranslatableString GetPluginFormatDescription() override;
176 
177    ///! Probes the file and opens it if appropriate
178    std::unique_ptr<ImportFileHandle> Open(
179       const FilePath &Filename, AudacityProject*) override;
180 };
181 
182 struct StreamContext final
183 {
184    int StreamIndex { -1 };
185 
186    std::unique_ptr<AVCodecContextWrapper> CodecContext;
187 
188    int InitialChannels { 0 };
189    sampleFormat SampleFormat { floatSample };
190 
191    bool Use { true };
192 };
193 
194 ///! Does actual import, returned by FFmpegImportPlugin::Open
195 class FFmpegImportFileHandle final : public ImportFileHandle
196 {
197 
198 public:
199    FFmpegImportFileHandle(const FilePath & name);
200    ~FFmpegImportFileHandle();
201 
202    ///! Format initialization
203    ///\return true if successful, false otherwise
204    bool Init();
205    ///! Codec initialization
206    ///\return true if successful, false otherwise
207    bool InitCodecs();
208 
209 
210    TranslatableString GetFileDescription() override;
211    ByteCount GetFileUncompressedBytes() override;
212 
213    ///! Imports audio
214    ///\return import status (see Import.cpp)
215    ProgressResult Import(WaveTrackFactory *trackFactory, TrackHolders &outTracks,
216       Tags *tags) override;
217 
218    ///! Writes decoded data into WaveTracks.
219    ///\param sc - stream context
220    ProgressResult WriteData(StreamContext* sc, const AVPacketWrapper* packet);
221 
222    ///! Writes extracted metadata to tags object
223    ///\param avf - file context
224    ///\ tags - Audacity tags object
225    void WriteMetadata(Tags *tags);
226 
227    ///! Retrieves metadata from FFmpeg and converts to wxString
228    ///\param avf - file context
229    ///\ tags - Audacity tags object
230    ///\ tag - name of tag to set
231    ///\ name - name of metadata item to retrieve
232    void GetMetadata(Tags &tags, const wxChar *tag, const char *name);
233 
234    ///! Called by Import.cpp
235    ///\return number of readable streams in the file
GetStreamCount()236    wxInt32 GetStreamCount() override
237    {
238       return static_cast<wxInt32>(mStreamContexts.size());
239    }
240 
241    ///! Called by Import.cpp
242    ///\return array of strings - descriptions of the streams
GetStreamInfo()243    const TranslatableStrings &GetStreamInfo() override
244    {
245       return mStreamInfo;
246    }
247 
248    ///! Called by Import.cpp
249    ///\param StreamID - index of the stream in mStreamInfo and mStreamContexts
250    ///\param Use - true if this stream should be imported, false otherwise
SetStreamUsage(wxInt32 StreamID,bool Use)251    void SetStreamUsage(wxInt32 StreamID, bool Use) override
252    {
253       if (StreamID < static_cast<wxInt32>(mStreamContexts.size()))
254          mStreamContexts[StreamID].Use = Use;
255    }
256 
257 private:
258    // Construct this member first, so it is destroyed last, so the functions
259    // remain loaded while other members are destroyed
260    const std::shared_ptr<FFmpegFunctions> mFFmpeg = FFmpegFunctions::Load();
261 
262    std::vector<StreamContext> mStreamContexts;
263 
264    std::unique_ptr<AVFormatContextWrapper> mAVFormatContext;
265 
266    TranslatableStrings   mStreamInfo;    //!< Array of stream descriptions. After Init() and before Import(), same size as mStreamContexts
267 
268    wxInt64               mProgressPos = 0;   //!< Current timestamp, file position or whatever is used as first argument for Update()
269    wxInt64               mProgressLen = 1;   //!< Duration, total length or whatever is used as second argument for Update()
270 
271    bool                  mCancelled = false;     //!< True if importing was canceled by user
272    bool                  mStopped = false;       //!< True if importing was stopped by user
273    const FilePath        mName;
274    TrackHolders mChannels;               //!< 2-dimensional array of WaveTracks.
275                                          //!< First dimension - streams,
276                                          //!< After Import(), same size as mStreamContexts;
277                                          //!< second - channels of a stream.
278 };
279 
280 
GetPluginFormatDescription()281 TranslatableString FFmpegImportPlugin::GetPluginFormatDescription()
282 {
283    return DESC;
284 }
285 
Open(const FilePath & filename,AudacityProject *)286 std::unique_ptr<ImportFileHandle> FFmpegImportPlugin::Open(
287    const FilePath &filename, AudacityProject*)
288 {
289    auto ffmpeg = FFmpegFunctions::Load();
290 
291    //Check if we're loading explicitly supported format
292    wxString extension = filename.AfterLast(wxT('.'));
293    if (SupportsExtension(extension))
294    {
295       //Audacity is trying to load something that is declared as
296       //officially supported by this plugin.
297       //If we don't have FFmpeg configured - tell the user about it.
298       //Since this will be happening often, use disableable "FFmpeg not found" dialog
299       //insdead of usual AudacityMessageBox()
300       bool newsession = NewImportingSession.Read();
301       if (!ffmpeg)
302       {
303          auto dontShowDlg = FFmpegNotFoundDontShow.Read();
304          if (dontShowDlg == 0 && newsession)
305          {
306             NewImportingSession.Write(false);
307             gPrefs->Flush();
308             FFmpegNotFoundDialog{ nullptr }.ShowModal();
309 
310             ffmpeg = FFmpegFunctions::Load();
311          }
312       }
313    }
314    if (!ffmpeg)
315    {
316       return nullptr;
317    }
318 
319    // Construct the handle only after any reloading of ffmpeg functions
320    auto handle = std::make_unique<FFmpegImportFileHandle>(filename);
321 
322    // Open the file for import
323    bool success = handle->Init();
324 
325    if (!success) {
326       return nullptr;
327    }
328 
329    return handle;
330 }
331 
332 static Importer::RegisteredImportPlugin registered{ "FFmpeg",
333    std::make_unique< FFmpegImportPlugin >()
334 };
335 
336 
FFmpegImportFileHandle(const FilePath & name)337 FFmpegImportFileHandle::FFmpegImportFileHandle(const FilePath & name)
338 :  ImportFileHandle(name)
339 ,  mName{ name }
340 {
341 }
342 
Init()343 bool FFmpegImportFileHandle::Init()
344 {
345    if (!mFFmpeg)
346       return false;
347 
348    mAVFormatContext = mFFmpeg->CreateAVFormatContext();
349 
350    const auto err = mAVFormatContext->OpenInputContext(mName, nullptr, AVDictionaryWrapper(*mFFmpeg));
351 
352    if (err != AVIOContextWrapper::OpenResult::Success)
353    {
354       wxLogError(wxT("FFmpeg : AVFormatContextWrapper::OpenInputContext() failed for file %s"), mName);
355       return false;
356    }
357 
358    if (!InitCodecs())
359       return false;
360 
361    return true;
362 }
363 
InitCodecs()364 bool FFmpegImportFileHandle::InitCodecs()
365 {
366    for (unsigned int i = 0; i < mAVFormatContext->GetStreamsCount(); i++)
367    {
368       const AVStreamWrapper* stream = mAVFormatContext->GetStream(i);
369 
370       if (stream->IsAudio())
371       {
372          const AVCodecIDFwd id = mAVFormatContext->GetStream(i)->GetAVCodecID();
373 
374          auto codec = mFFmpeg->CreateDecoder(id);
375          auto name = mFFmpeg->avcodec_get_name(id);
376 
377          if (codec == NULL)
378          {
379             wxLogError(
380                wxT("FFmpeg : CreateDecoder() failed. Index[%02d], Codec[%02x - %s]"),
381                i, id, name);
382             //FFmpeg can't decode this stream, skip it
383             continue;
384          }
385 
386          auto codecContextPtr = stream->GetAVCodecContext();
387 
388          if ( codecContextPtr->Open( codecContextPtr->GetCodec() ) < 0 )
389          {
390             wxLogError(wxT("FFmpeg : Open() failed. Index[%02d], Codec[%02x - %s]"),i,id,name);
391             //Can't open decoder - skip this stream
392             continue;
393          }
394 
395          const int channels = codecContextPtr->GetChannels();
396          const sampleFormat preferredFormat =
397             codecContextPtr->GetPreferredAudacitySampleFormat();
398 
399          auto codecContext = codecContextPtr.get();
400 
401          mStreamContexts.emplace_back(
402             StreamContext { stream->GetIndex(), std::move(codecContextPtr),
403                             channels, preferredFormat, true });
404 
405          // Stream is decodeable and it is audio. Add it and its description to the arrays
406          int duration = 0;
407          if (stream->GetDuration() > 0)
408             duration = stream->GetDuration() * stream->GetTimeBase().num / stream->GetTimeBase().den;
409          else
410             duration = mAVFormatContext->GetDuration() / AUDACITY_AV_TIME_BASE;
411 
412          wxString bitrate;
413          if (codecContext->GetBitRate() > 0)
414             bitrate.Printf(wxT("%d"),(int)codecContext->GetBitRate());
415          else
416             bitrate.Printf(wxT("?"));
417 
418          AVDictionaryWrapper streamMetadata = stream->GetMetadata();
419 
420          auto lang = std::string(streamMetadata.Get("language", {}));
421 
422          auto strinfo = XO(
423 /* i18n-hint: "codec" is short for a "coder-decoder" algorithm */
424 "Index[%02x] Codec[%s], Language[%s], Bitrate[%s], Channels[%d], Duration[%d]")
425             .Format(
426                stream->GetIndex(),
427                name,
428                lang,
429                bitrate,
430                (int)codecContext->GetChannels(),
431                (int)duration);
432 
433          mStreamInfo.push_back(strinfo);
434       }
435       //for video and unknown streams do nothing
436    }
437    //It doesn't really returns false, but GetStreamCount() will return 0 if file is composed entirely of unreadable streams
438    return true;
439 }
440 
GetFileDescription()441 TranslatableString FFmpegImportFileHandle::GetFileDescription()
442 {
443    return DESC;
444 }
445 
446 
GetFileUncompressedBytes()447 auto FFmpegImportFileHandle::GetFileUncompressedBytes() -> ByteCount
448 {
449    // TODO: Get Uncompressed byte count.
450    return 0;
451 }
452 
Import(WaveTrackFactory * trackFactory,TrackHolders & outTracks,Tags * tags)453 ProgressResult FFmpegImportFileHandle::Import(WaveTrackFactory *trackFactory,
454               TrackHolders &outTracks,
455               Tags *tags)
456 {
457    outTracks.clear();
458 
459    CreateProgress();
460 
461    //! This may break the correspondence with mStreamInfo
462    mStreamContexts.erase (std::remove_if (mStreamContexts.begin (), mStreamContexts.end (), [](const StreamContext& ctx) {
463       return !ctx.Use;
464    }), mStreamContexts.end());
465 
466    mChannels.resize(mStreamContexts.size());
467 
468    int s = -1;
469    for (auto &stream : mChannels)
470    {
471       ++s;
472 
473       const StreamContext& sc = mStreamContexts[s];
474 
475       stream.resize(sc.InitialChannels);
476 
477       for (auto &channel : stream)
478          channel = NewWaveTrack(*trackFactory, sc.SampleFormat, sc.CodecContext->GetSampleRate());
479    }
480 
481    // Handles the start_time by creating silence. This may or may not be correct.
482    // There is a possibility that we should ignore first N milliseconds of audio instead. I do not know.
483    /// TODO: Nag FFmpeg devs about start_time until they finally say WHAT is this and HOW to handle it.
484    s = -1;
485    for (auto &stream : mChannels)
486    {
487       ++s;
488 
489       int64_t stream_delay = 0;
490       const auto& sc = mStreamContexts[s];
491 
492       const int64_t streamStartTime =
493          mAVFormatContext->GetStream(sc.StreamIndex)->GetStartTime();
494 
495       if (streamStartTime != int64_t(AUDACITY_AV_NOPTS_VALUE) && streamStartTime > 0)
496       {
497          stream_delay = streamStartTime;
498 
499          wxLogDebug(
500             wxT("Stream %d start_time = %lld, that would be %f milliseconds."),
501             s, (long long)streamStartTime, double(streamStartTime) / 1000);
502       }
503 
504       if (stream_delay > 0)
505       {
506          int c = -1;
507          for (auto &channel : stream)
508          {
509             ++c;
510 
511             WaveTrack *t = channel.get();
512             t->InsertSilence(0,double(stream_delay)/AUDACITY_AV_TIME_BASE);
513          }
514       }
515    }
516    // This is the heart of the importing process
517    // The result of Import() to be returned. It will be something other than zero if user canceled or some error appears.
518    auto res = ProgressResult::Success;
519 
520    // Read frames.
521    for (std::unique_ptr<AVPacketWrapper> packet;
522         (packet = mAVFormatContext->ReadNextPacket()) != nullptr &&
523         (res == ProgressResult::Success);)
524    {
525       // Find a matching StreamContext
526       auto streamContextIt = std::find_if(
527          mStreamContexts.begin(), mStreamContexts.end(),
528          [index = packet->GetStreamIndex()](const StreamContext& ctx)
529          { return ctx.StreamIndex == index;
530       });
531 
532       if (streamContextIt == mStreamContexts.end())
533          continue;
534 
535       res = WriteData(&(*streamContextIt), packet.get());
536    }
537 
538    // Flush the decoders.
539    if (!mStreamContexts.empty() && (res == ProgressResult::Success || res == ProgressResult::Stopped))
540    {
541       auto emptyPacket = mFFmpeg->CreateAVPacketWrapper();
542 
543       for (StreamContext& sc : mStreamContexts)
544          WriteData(&sc, emptyPacket.get());
545    }
546 
547    // Something bad happened - destroy everything!
548    if (res == ProgressResult::Cancelled || res == ProgressResult::Failed)
549       return res;
550    //else if (res == 2), we just stop the decoding as if the file has ended
551 
552    // Copy audio from mChannels to newly created tracks (destroying mChannels elements in process)
553    for (auto &stream : mChannels)
554       for(auto &channel : stream)
555          channel->Flush();
556 
557    outTracks.swap(mChannels);
558 
559    // Save metadata
560    WriteMetadata(tags);
561 
562    return res;
563 }
564 
WriteData(StreamContext * sc,const AVPacketWrapper * packet)565 ProgressResult FFmpegImportFileHandle::WriteData(StreamContext *sc, const AVPacketWrapper* packet)
566 {
567    // Find the stream index in mStreamContexts array
568    int streamid = -1;
569    auto iter = mChannels.begin();
570 
571    for (int i = 0; i < static_cast<int>(mStreamContexts.size()); ++iter, ++i)
572    {
573       if (&mStreamContexts[i] == sc)
574       {
575          streamid = i;
576          break;
577       }
578    }
579    // Stream is not found. This should not really happen
580    if (streamid == -1)
581    {
582       return ProgressResult::Success;
583    }
584 
585    size_t nChannels = std::min(sc->CodecContext->GetChannels(), sc->InitialChannels);
586 
587    if (sc->SampleFormat == int16Sample)
588    {
589       auto data = sc->CodecContext->DecodeAudioPacketInt16(packet);
590 
591       const int channelsCount = sc->CodecContext->GetChannels();
592       const int samplesPerChannel = data.size() / channelsCount;
593 
594       // Write audio into WaveTracks
595       auto iter2 = iter->begin();
596       for (size_t chn = 0; chn < nChannels; ++iter2, ++chn)
597       {
598          iter2->get()->Append(
599             reinterpret_cast<samplePtr>(data.data() + chn), sc->SampleFormat,
600             samplesPerChannel,
601             sc->CodecContext->GetChannels());
602       }
603    }
604    else if (sc->SampleFormat == floatSample)
605    {
606       auto data = sc->CodecContext->DecodeAudioPacketFloat(packet);
607 
608       const int channelsCount = sc->CodecContext->GetChannels();
609       const int samplesPerChannel = data.size() / channelsCount;
610 
611       // Write audio into WaveTracks
612       auto iter2 = iter->begin();
613       for (size_t chn = 0; chn < nChannels; ++iter2, ++chn)
614       {
615          iter2->get()->Append(
616             reinterpret_cast<samplePtr>(data.data() + chn), sc->SampleFormat,
617             samplesPerChannel, sc->CodecContext->GetChannels());
618       }
619    }
620 
621    const AVStreamWrapper* avStream = mAVFormatContext->GetStream(sc->StreamIndex);
622 
623    // Try to update the progress indicator (and see if user wants to cancel)
624    auto updateResult = ProgressResult::Success;
625    int64_t filesize = mFFmpeg->avio_size(mAVFormatContext->GetAVIOContext()->GetWrappedValue());
626    // PTS (presentation time) is the proper way of getting current position
627    if (
628       packet->GetPresentationTimestamp() != AUDACITY_AV_NOPTS_VALUE &&
629       mAVFormatContext->GetDuration() != AUDACITY_AV_NOPTS_VALUE)
630    {
631       auto timeBase = avStream->GetTimeBase();
632 
633       mProgressPos =
634          packet->GetPresentationTimestamp() * timeBase.num / timeBase.den;
635 
636       mProgressLen =
637          (mAVFormatContext->GetDuration() > 0 ?
638              mAVFormatContext->GetDuration() / AUDACITY_AV_TIME_BASE :
639              1);
640    }
641    // When PTS is not set, use number of frames and number of current frame
642    else if (
643       avStream->GetFramesCount() > 0 && sc->CodecContext->GetFrameNumber() > 0 &&
644       sc->CodecContext->GetFrameNumber() <= avStream->GetFramesCount())
645    {
646       mProgressPos = sc->CodecContext->GetFrameNumber();
647       mProgressLen = avStream->GetFramesCount();
648    }
649    // When number of frames is unknown, use position in file
650    else if (
651       filesize > 0 && packet->GetPos() > 0 && packet->GetPos() <= filesize)
652    {
653       mProgressPos = packet->GetPos();
654       mProgressLen = filesize;
655    }
656    updateResult = mProgress->Update(mProgressPos, mProgressLen != 0 ? mProgressLen : 1);
657 
658    return updateResult;
659 }
660 
WriteMetadata(Tags * tags)661 void FFmpegImportFileHandle::WriteMetadata(Tags *tags)
662 {
663    Tags temp;
664 
665    GetMetadata(temp, TAG_TITLE, "title");
666    GetMetadata(temp, TAG_COMMENTS, "comment");
667    GetMetadata(temp, TAG_ALBUM, "album");
668    GetMetadata(temp, TAG_TRACK, "track");
669    GetMetadata(temp, TAG_GENRE, "genre");
670 
671    if (wxString(mAVFormatContext->GetInputFormat()->GetName()).Contains("m4a"))
672    {
673       GetMetadata(temp, TAG_ARTIST, "artist");
674       GetMetadata(temp, TAG_YEAR, "date");
675    }
676    else if (wxString(mAVFormatContext->GetInputFormat()->GetName()).Contains("asf")) /* wma */
677    {
678       GetMetadata(temp, TAG_ARTIST, "artist");
679       GetMetadata(temp, TAG_YEAR, "year");
680    }
681    else
682    {
683       GetMetadata(temp, TAG_ARTIST, "author");
684       GetMetadata(temp, TAG_YEAR, "year");
685    }
686 
687    if (!temp.IsEmpty())
688    {
689       *tags = temp;
690    }
691 }
692 
GetMetadata(Tags & tags,const wxChar * tag,const char * name)693 void FFmpegImportFileHandle::GetMetadata(Tags &tags, const wxChar *tag, const char *name)
694 {
695    auto metadata = mAVFormatContext->GetMetadata();
696 
697    if (metadata.HasValue(name, DICT_IGNORE_SUFFIX))
698       tags.SetTag(tag, wxString::FromUTF8(std::string(metadata.Get(name, {}, DICT_IGNORE_SUFFIX))));
699 }
700 
701 
~FFmpegImportFileHandle()702 FFmpegImportFileHandle::~FFmpegImportFileHandle()
703 {
704 
705 }
706 
707 #endif //USE_FFMPEG
708