1 /******************************************************************************
2     QtAV:  Multimedia framework based on Qt and FFmpeg
3     Copyright (C) 2012-2018 Wang Bin <wbsecg1@gmail.com>
4 
5 *   This file is part of QtAV
6 
7     This library is free software; you can redistribute it and/or
8     modify it under the terms of the GNU Lesser General Public
9     License as published by the Free Software Foundation; either
10     version 2.1 of the License, or (at your option) any later version.
11 
12     This library is distributed in the hope that it will be useful,
13     but WITHOUT ANY WARRANTY; without even the implied warranty of
14     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15     Lesser General Public License for more details.
16 
17     You should have received a copy of the GNU Lesser General Public
18     License along with this library; if not, write to the Free Software
19     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
20 ******************************************************************************/
21 #include "QtAV/AVDemuxer.h"
22 #include "QtAV/MediaIO.h"
23 #include "QtAV/private/AVCompat.h"
24 #include <QtCore/QMutex>
25 #include <QtCore/QStringList>
26 #if QT_VERSION >= QT_VERSION_CHECK(4, 7, 0)
27 #include <QtCore/QElapsedTimer>
28 #else
29 #include <QtCore/QTime>
30 typedef QTime QElapsedTimer;
31 #endif
32 #include "utils/internal.h"
33 #include "utils/Logger.h"
34 
35 namespace QtAV {
36 static const char kFileScheme[] = "file:";
37 
38 class AVDemuxer::InterruptHandler : public AVIOInterruptCB
39 {
40 public:
41     enum Action {
42         Unknown = -1,
43         Open,
44         FindStreamInfo,
45         Read
46     };
47     //default network timeout: 30000
InterruptHandler(AVDemuxer * demuxer,int timeout=30000)48     InterruptHandler(AVDemuxer* demuxer, int timeout = 30000)
49       : mStatus(0)
50       , mTimeout(timeout)
51       , mTimeoutAbort(true)
52       , mEmitError(true)
53       //, mLastTime(0)
54       , mAction(Unknown)
55       , mpDemuxer(demuxer)
56     {
57         callback = handleTimeout;
58         opaque = this;
59     }
~InterruptHandler()60     ~InterruptHandler() {
61 #if QT_VERSION >= QT_VERSION_CHECK(4, 7, 0)
62         mTimer.invalidate();
63 #else
64         mTimer.stop();
65 #endif
66     }
begin(Action act)67     void begin(Action act) {
68         if (mStatus > 0)
69             mStatus = 0;
70         mEmitError = true;
71         mAction = act;
72         mTimer.start();
73     }
end()74     void end() {
75 #if QT_VERSION >= QT_VERSION_CHECK(4, 7, 0)
76         mTimer.invalidate();
77 #else
78         mTimer.stop();
79 #endif
80         switch (mAction) {
81         case Read:
82             //mpDemuxer->setMediaStatus(BufferedMedia);
83             break;
84         default:
85             break;
86         }
87         mAction = Unknown;
88     }
getTimeout() const89     qint64 getTimeout() const { return mTimeout; }
setTimeout(qint64 timeout)90     void setTimeout(qint64 timeout) { mTimeout = timeout; }
setInterruptOnTimeout(bool value)91     bool setInterruptOnTimeout(bool value) {
92         if (mTimeoutAbort == value)
93             return false;
94         mTimeoutAbort = value;
95         if (mTimeoutAbort) {
96             mEmitError = true;
97         }
98         return true;
99     }
isInterruptOnTimeout() const100     bool isInterruptOnTimeout() const {return mTimeoutAbort;}
getStatus() const101     int getStatus() const { return mStatus; }
setStatus(int status)102     void setStatus(int status) { mStatus = status; }
103     /*
104      * metodo per interruzione loop ffmpeg
105      * @param void*obj: classe attuale
106       * @return
107      *  >0 Interruzione loop di ffmpeg!
108     */
handleTimeout(void * obj)109     static int handleTimeout(void* obj) {
110         InterruptHandler* handler = static_cast<InterruptHandler*>(obj);
111         if (!handler) {
112             qWarning("InterruptHandler is null");
113             return -1;
114         }
115         //check manual interruption
116         if (handler->getStatus() < 0) {
117             qDebug("User Interrupt: -> quit!");
118             // DO NOT call setMediaStatus() here.
119             /* MUST make sure blocking functions (open, read) return before we change the status
120              * because demuxer may be closed in another thread at the same time if status is not LoadingMedia
121              * use handleError() after blocking functions return is good
122              */
123             // 1: blocking operation will be aborted.
124             return 1;//interrupt
125         }
126         // qApp->processEvents(); //FIXME: qml crash
127         switch (handler->mAction) {
128         case Unknown: //callback is not called between begin()/end()
129             //qWarning("Unknown timeout action");
130             break;
131         case Open:
132         case FindStreamInfo:
133             //qDebug("set loading media for %d from: %d", handler->mAction, handler->mpDemuxer->mediaStatus());
134             handler->mpDemuxer->setMediaStatus(LoadingMedia);
135             break;
136         case Read:
137             //handler->mpDemuxer->setMediaStatus(BufferingMedia);
138         default:
139             break;
140         }
141         if (handler->mTimeout < 0)
142             return 0;
143         if (!handler->mTimer.isValid()) {
144             //qDebug("timer is not valid, start it");
145             handler->mTimer.start();
146             //handler->mLastTime = handler->mTimer.elapsed();
147             return 0;
148         }
149         //use restart
150 #if QT_VERSION >= QT_VERSION_CHECK(4, 7, 0)
151         if (!handler->mTimer.hasExpired(handler->mTimeout))
152 #else
153         if (handler->mTimer.elapsed() < handler->mTimeout)
154 #endif
155             return 0;
156         qDebug("status: %d, Timeout expired: %lld/%lld -> quit!", (int)handler->mStatus, handler->mTimer.elapsed(), handler->mTimeout);
157         handler->mTimer.invalidate();
158         if (handler->mStatus == 0) {
159             AVError::ErrorCode ec(AVError::ReadTimedout);
160             if (handler->mAction == Open) {
161                 ec = AVError::OpenTimedout;
162             } else if (handler->mAction == FindStreamInfo) {
163                 ec = AVError::ParseStreamTimedOut;
164             } else if (handler->mAction == Read) {
165                 ec = AVError::ReadTimedout;
166             }
167             handler->mStatus = (int)ec;
168             // maybe changed in other threads
169             //handler->mStatus.testAndSetAcquire(0, ec);
170         }
171         if (handler->mTimeoutAbort)
172             return 1;
173         // emit demuxer error, handleerror
174         if (handler->mEmitError) {
175             handler->mEmitError = false;
176             AVError::ErrorCode ec = AVError::ErrorCode(handler->mStatus); //FIXME: maybe changed in other threads
177             QString es;
178             handler->mpDemuxer->handleError(AVERROR_EXIT, &ec, es);
179         }
180         return 0;
181     }
182 private:
183     int mStatus;
184     qint64 mTimeout;
185     bool mTimeoutAbort;
186     bool mEmitError;
187     //qint64 mLastTime;
188     Action mAction;
189     AVDemuxer *mpDemuxer;
190     QElapsedTimer mTimer;
191 };
192 
193 class AVDemuxer::Private
194 {
195 public:
Private()196     Private()
197         : media_status(NoMedia)
198         , seekable(false)
199         , network(false)
200         , has_attached_pic(false)
201         , started(false)
202         , max_pts(0.0)
203         , eof(false)
204         , media_changed(true)
205         , buf_pos(0)
206         , stream(-1)
207         , format_ctx(0)
208         , input_format(0)
209         , input(0)
210         , seek_unit(SeekByTime)
211         , seek_type(AccurateSeek)
212         , dict(0)
213         , interrupt_hanlder(0)
214     {}
~Private()215     ~Private() {
216         delete interrupt_hanlder;
217         if (dict) {
218             av_dict_free(&dict);
219             dict = 0;
220         }
221         if (input) {
222             delete input;
223             input = 0;
224         }
225     }
226     void applyOptionsForDict();
227     void applyOptionsForContext();
resetStreams()228     void resetStreams() {
229         stream = -1;
230         if (media_changed)
231             astream = vstream = sstream = StreamInfo();
232         else
233             astream.avctx = vstream.avctx = sstream.avctx = 0;
234         audio_streams.clear();
235         video_streams.clear();
236         subtitle_streams.clear();
237     }
checkNetwork()238     void checkNetwork() {
239         // FIXME: is there a good way to check network? now use URLContext.flags == URL_PROTOCOL_FLAG_NETWORK
240         // not network: concat cache pipe avdevice crypto?
241         if (!file.isEmpty()
242                 && file.contains(QLatin1String(":"))
243                 && (file.startsWith(QLatin1String("http")) //http, https, httpproxy
244                 || file.startsWith(QLatin1String("rtmp")) //rtmp{,e,s,te,ts}
245                 || file.startsWith(QLatin1String("mms")) //mms{,h,t}
246                 || file.startsWith(QLatin1String("ffrtmp")) //ffrtmpcrypt, ffrtmphttp
247                 || file.startsWith(QLatin1String("rtp:"))
248                 || file.startsWith(QLatin1String("rtsp:"))
249                 || file.startsWith(QLatin1String("sctp:"))
250                 || file.startsWith(QLatin1String("tcp:"))
251                 || file.startsWith(QLatin1String("tls:"))
252                 || file.startsWith(QLatin1String("udp:"))
253                 || file.startsWith(QLatin1String("gopher:"))
254                 )) {
255             network = true;
256         }
257     }
checkSeekable()258     bool checkSeekable() {
259         bool s = false;
260         if (!format_ctx)
261             return s;
262         // io.seekable: byte seeking
263         if (input)
264             s |= input->isSeekable();
265         if (format_ctx->pb)
266             s |= !!format_ctx->pb->seekable;
267         // format.read_seek: time seeking. For example, seeking on hls stream steps: find segment, read packet in segment and drop until desired pts got
268         s |= format_ctx->iformat->read_seek || format_ctx->iformat->read_seek2;
269         return s;
270     }
271     // set wanted_xx_stream. call openCodecs() to read new stream frames
272     // stream < 0 is choose best
273     bool setStream(AVDemuxer::StreamType st, int streamValue);
274     //called by loadFile(). if change to a new stream, call it(e.g. in AVPlayer)
275     bool prepareStreams();
276 
277     MediaStatus media_status;
278     bool seekable;
279     bool network;
280     bool has_attached_pic;
281     bool started;
282     qreal max_pts; // max pts read
283     bool eof;
284     bool media_changed;
285     mutable qptrdiff buf_pos; // detect eof for dynamic size (growing) stream even if detectDynamicStreamInterval() is not set
286     Packet pkt;
287     int stream;
288     QList<int> audio_streams, video_streams, subtitle_streams;
289     AVFormatContext *format_ctx;
290     //copy the info, not parse the file when constructed, then need member vars
291     QString file;
292     QString file_orig;
293     AVInputFormat *input_format;
294     QString format_forced;
295     MediaIO *input;
296 
297     SeekUnit seek_unit;
298     SeekType seek_type;
299 
300     AVDictionary *dict;
301     QVariantHash options;
302     typedef struct StreamInfo {
StreamInfoQtAV::AVDemuxer::Private::StreamInfo303         StreamInfo()
304             : stream(-1)
305             , wanted_stream(-1)
306             , index(-1)
307             , wanted_index(-1)
308             , avctx(0)
309         {}
310         // wanted_stream is REQUIRED. e.g. always set -1 to indicate the default stream, -2 to disable
311         int stream, wanted_stream; // -1 default, selected by ff
312         int index, wanted_index; // index in a kind of streams
313         AVCodecContext *avctx;
314     } StreamInfo;
315     StreamInfo astream, vstream, sstream;
316 
317     AVDemuxer::InterruptHandler *interrupt_hanlder;
318     QMutex mutex; //TODO: remove if load, read, seek is called in 1 thread
319 };
320 
AVDemuxer(QObject * parent)321 AVDemuxer::AVDemuxer(QObject *parent)
322     : QObject(parent)
323     , d(new Private())
324 {
325     // TODO: xxx_register_all already use static var
326     class AVInitializer {
327     public:
328         AVInitializer() {
329 #if !AVCODEC_STATIC_REGISTER
330             avcodec_register_all();
331 #endif
332 #if QTAV_HAVE(AVDEVICE)
333             avdevice_register_all();
334 #endif
335 #if !AVFORMAT_STATIC_REGISTER
336             av_register_all();
337 #endif
338             avformat_network_init();
339         }
340         ~AVInitializer() {
341             avformat_network_deinit();
342         }
343     };
344     static AVInitializer sAVInit;
345     Q_UNUSED(sAVInit);
346     d->interrupt_hanlder = new InterruptHandler(this);
347 }
348 
~AVDemuxer()349 AVDemuxer::~AVDemuxer()
350 {
351     unload();
352 }
353 
getFFmpegInputFormats(QStringList * formats,QStringList * extensions)354 static void getFFmpegInputFormats(QStringList* formats, QStringList* extensions)
355 {
356     static QStringList exts;
357     static QStringList fmts;
358     if (exts.isEmpty() && fmts.isEmpty()) {
359         QStringList e, f;
360 #if AVFORMAT_STATIC_REGISTER
361         const AVInputFormat *i = NULL;
362         void* it = NULL;
363         while ((i = av_demuxer_iterate(&it))) {
364 #else
365         AVInputFormat *i = NULL;
366         av_register_all(); // MUST register all input/output formats
367         while ((i = av_iformat_next(i))) {
368 #endif
369             if (i->extensions)
370                 e << QString::fromLatin1(i->extensions).split(QLatin1Char(','), QString::SkipEmptyParts);
371             if (i->name)
372                 f << QString::fromLatin1(i->name).split(QLatin1Char(','), QString::SkipEmptyParts);
373         }
374         foreach (const QString& v, e) {
375             exts.append(v.trimmed());
376         }
377         foreach (const QString& v, f) {
378             fmts.append(v.trimmed());
379         }
380         exts.removeDuplicates();
381         fmts.removeDuplicates();
382     }
383     if (formats)
384         *formats = fmts;
385     if (extensions)
386         *extensions = exts;
387 }
388 
389 const QStringList& AVDemuxer::supportedFormats()
390 {
391     static QStringList fmts;
392     if (fmts.isEmpty())
393         getFFmpegInputFormats(&fmts, NULL);
394     return fmts;
395 }
396 
397 const QStringList& AVDemuxer::supportedExtensions()
398 {
399     static QStringList exts;
400     if (exts.isEmpty())
401         getFFmpegInputFormats(NULL, &exts);
402     return exts;
403 }
404 
405 const QStringList &AVDemuxer::supportedProtocols()
406 {
407     static QStringList protocols;
408     if (!protocols.isEmpty())
409         return protocols;
410 #if QTAV_HAVE(AVDEVICE)
411     protocols << QStringLiteral("avdevice");
412 #endif
413 #if !AVFORMAT_STATIC_REGISTER
414     av_register_all(); // MUST register all input/output formats
415 #endif
416     void* opq = 0;
417     const char* protocol = avio_enum_protocols(&opq, 0);
418     while (protocol) {
419         // static string, no deep copy needed. but QByteArray::fromRawData(data,size) assumes data is not null terminated and we must give a size
420         protocols.append(QString::fromUtf8(protocol));
421         protocol = avio_enum_protocols(&opq, 0);
422     }
423     return protocols;
424 }
425 
426 MediaStatus AVDemuxer::mediaStatus() const
427 {
428     return d->media_status;
429 }
430 
431 bool AVDemuxer::readFrame()
432 {
433     QMutexLocker lock(&d->mutex);
434     Q_UNUSED(lock);
435     if (!d->format_ctx)
436         return false;
437     d->pkt = Packet();
438     // no lock required because in AVDemuxThread read and seek are in the same thread
439     AVPacket packet;
440     av_init_packet(&packet);
441     d->interrupt_hanlder->begin(InterruptHandler::Read);
442     int ret = av_read_frame(d->format_ctx, &packet); //0: ok, <0: error/end
443     d->interrupt_hanlder->end();
444     // TODO: why return 0 if interrupted by user?
445     if (ret < 0) {
446         //end of file. FIXME: why no d->eof if replaying by seek(0)?
447         // ffplay also check pb && pb->error and exit read thread
448         if (ret == AVERROR_EOF
449                 || avio_feof(d->format_ctx->pb)) {
450             if (!d->eof) {
451                 if (getInterruptStatus()) {
452                     AVError::ErrorCode ec(AVError::ReadError);
453                     QString msg(tr("error reading stream data"));
454                     handleError(ret, &ec, msg);
455                 }
456                 if (mediaStatus() != StalledMedia) {
457                     d->eof = true;
458 #if 0 // EndOfMedia when demux thread finished
459                     d->started = false;
460                     setMediaStatus(EndOfMedia);
461                     Q_EMIT finished();
462 #endif
463                     qDebug("End of file. erreof=%d feof=%d", ret == AVERROR_EOF, avio_feof(d->format_ctx->pb));
464                 }
465             }
466             av_packet_unref(&packet); //important!
467             return false;
468         }
469         if (ret == AVERROR(EAGAIN)) {
470             qWarning("demuxer EAGAIN :%s", av_err2str(ret));
471             av_packet_unref(&packet); //important!
472             return false;
473         }
474         AVError::ErrorCode ec(AVError::ReadError);
475         QString msg(tr("error reading stream data"));
476         handleError(ret, &ec, msg);
477         qWarning("[AVDemuxer] error: %s", av_err2str(ret));
478         av_packet_unref(&packet); //important!
479         return false;
480     }
481     d->stream = packet.stream_index;
482     //check whether the 1st frame is alreay got. emit only once
483     if (!d->started) {
484         d->started = true;
485         Q_EMIT started();
486     }
487     if (d->stream != videoStream() && d->stream != audioStream() && d->stream != subtitleStream()) {
488         //qWarning("[AVDemuxer] unknown stream index: %d", stream);
489         av_packet_unref(&packet); //important!
490         return false;
491     }
492     // TODO: v4l2 copy
493     d->pkt = Packet::fromAVPacket(&packet, av_q2d(d->format_ctx->streams[d->stream]->time_base));
494     av_packet_unref(&packet); //important!
495     d->eof = false;
496     if (d->pkt.pts > qreal(duration())/1000.0) {
497         d->max_pts = d->pkt.pts;
498     }
499     return true;
500 }
501 
502 Packet AVDemuxer::packet() const
503 {
504     return d->pkt;
505 }
506 
507 int AVDemuxer::stream() const
508 {
509     return d->stream;
510 }
511 
512 bool AVDemuxer::atEnd() const
513 {
514     if (!d->format_ctx)
515         return false;
516     if (d->format_ctx->pb)  {
517         AVIOContext *pb = d->format_ctx->pb;
518         //qDebug("pb->error: %#x, eof: %d, pos: %lld, bufptr: %p", pb->error, pb->eof_reached, pb->pos, pb->buf_ptr);
519         if (d->eof && (qptrdiff)pb->buf_ptr == d->buf_pos)
520             return true;
521         d->buf_pos = (qptrdiff)pb->buf_ptr;
522         return false;
523     }
524     return d->eof;
525 }
526 
527 bool AVDemuxer::isSeekable() const
528 {
529     return d->seekable;
530 }
531 
532 void AVDemuxer::setSeekUnit(SeekUnit unit)
533 {
534     d->seek_unit = unit;
535 }
536 
537 SeekUnit AVDemuxer::seekUnit() const
538 {
539     return d->seek_unit;
540 }
541 
542 void AVDemuxer::setSeekType(SeekType target)
543 {
544     d->seek_type = target;
545 }
546 
547 SeekType AVDemuxer::seekType() const
548 {
549     return d->seek_type;
550 }
551 
552 //TODO: seek by byte
553 bool AVDemuxer::seek(qint64 pos)
554 {
555     if (!isLoaded())
556         return false;
557     //duration: unit is us (10^-6 s, AV_TIME_BASE)
558     qint64 upos = pos*1000LL; // TODO: av_rescale
559     if (upos > startTimeUs() + durationUs() || pos < 0LL) {
560         if (pos >= 0LL && d->input && d->input->isSeekable() && d->input->isVariableSize()) {
561             qDebug("Seek for variable size hack. %lld %.2f. valid range [%lld, %lld]", upos, double(upos)/double(durationUs()), startTimeUs(), startTimeUs()+durationUs());
562         } else if (d->max_pts > qreal(duration())/1000.0) { //FIXME
563             qDebug("Seek (%lld) when video duration is growing %lld=>%lld", pos, duration(), qint64(d->max_pts*1000.0));
564         } else {
565             qWarning("Invalid seek position %lld %.2f. valid range [%lld, %lld]", upos, double(upos)/double(durationUs()), startTimeUs(), startTimeUs()+durationUs());
566             return false;
567         }
568     }
569     d->eof = false;
570     // no lock required because in AVDemuxThread read and seek are in the same thread
571 #if 0
572     //t: unit is s
573     qreal t = q;// * (double)d->format_ctx->duration; //
574     int ret = av_seek_frame(d->format_ctx, -1, (int64_t)(t*AV_TIME_BASE), t > d->pkt.pts ? 0 : AVSEEK_FLAG_BACKWARD);
575     qDebug("[AVDemuxer] seek to %f %f %lld / %lld", q, d->pkt.pts, (int64_t)(t*AV_TIME_BASE), durationUs());
576 #else
577     //TODO: d->pkt.pts may be 0, compute manually.
578 
579     bool backward = d->seek_type == AccurateSeek || upos <= (int64_t)(d->pkt.pts*AV_TIME_BASE);
580     //qDebug("[AVDemuxer] seek to %f %f %lld / %lld backward=%d", double(upos)/double(durationUs()), d->pkt.pts, upos, durationUs(), backward);
581     //AVSEEK_FLAG_BACKWARD has no effect? because we know the timestamp
582     // FIXME: back flag is opposite? otherwise seek is bad and may crash?
583     /* If stread->inputdex is (-1), a default
584      * stream is selected, and timestamp is automatically converted
585      * from AV_TIME_BASE units to the stream specific time_base.
586      */
587     int seek_flag = (backward ? AVSEEK_FLAG_BACKWARD : 0);
588     if (d->seek_type == AccurateSeek) {
589         seek_flag = AVSEEK_FLAG_BACKWARD;
590     }
591     if (d->seek_type == AnyFrameSeek) {
592         seek_flag |= AVSEEK_FLAG_ANY;
593     }
594     //qDebug("seek flag: %d", seek_flag);
595     //bool seek_bytes = !!(d->format_ctx->iformat->flags & AVFMT_TS_DISCONT) && strcmp("ogg", d->format_ctx->iformat->name);
596     int ret = av_seek_frame(d->format_ctx, -1, upos, seek_flag);
597     //int ret = avformat_seek_file(d->format_ctx, -1, INT64_MIN, upos, upos, seek_flag);
598     //avformat_seek_file()
599     if (ret < 0 && (seek_flag & AVSEEK_FLAG_BACKWARD)) {
600         // seek to 0?
601         qDebug("av_seek_frame error with flag AVSEEK_FLAG_BACKWARD: %s. try to seek without the flag", av_err2str(ret));
602         seek_flag &= ~AVSEEK_FLAG_BACKWARD;
603         ret = av_seek_frame(d->format_ctx, -1, upos, seek_flag);
604     }
605     //qDebug("av_seek_frame ret: %d", ret);
606 #endif
607     if (ret < 0) {
608         AVError::ErrorCode ec(AVError::SeekError);
609         QString msg(tr("seek error"));
610         handleError(ret, &ec, msg);
611         return false;
612     }
613     // TODO: replay
614     if (upos <= startTime()) {
615         qDebug("************seek to beginning. started = false");
616         d->started = false; //???
617         if (d->astream.avctx)
618             d->astream.avctx->frame_number = 0;
619         if (d->vstream.avctx)
620             d->vstream.avctx->frame_number = 0; //TODO: why frame_number not changed after seek?
621         if (d->sstream.avctx)
622             d->sstream.avctx->frame_number = 0;
623     }
624     return true;
625 }
626 
627 bool AVDemuxer::seek(qreal q)
628 {
629     if (duration() <= 0) {
630         qWarning("duration() must be valid for percentage seek");
631         return false;
632     }
633     return seek(qint64(q*(double)duration()));
634 }
635 
636 QString AVDemuxer::fileName() const
637 {
638     return d->file_orig;
639 }
640 
641 QIODevice* AVDemuxer::ioDevice() const
642 {
643     if (!d->input)
644         return 0;
645     if (d->input->name() != QLatin1String("QIODevice"))
646         return 0;
647     return d->input->property("device").value<QIODevice*>();
648 }
649 
650 MediaIO* AVDemuxer::mediaIO() const
651 {
652     return d->input;
653 }
654 
655 bool AVDemuxer::setMedia(const QString &fileName)
656 {
657     if (d->input) {
658         delete d->input;
659         d->input = 0;
660     }
661     d->file_orig = fileName;
662     const QString url_old(d->file);
663     d->file = fileName.trimmed();
664     if (d->file.startsWith(QLatin1String("mms:")))
665         d->file.insert(3, QLatin1Char('h'));
666     else if (d->file.startsWith(QLatin1String(kFileScheme)))
667         d->file = Internal::Path::toLocal(d->file);
668     int colon = d->file.indexOf(QLatin1Char(':'));
669     if (colon == 1) {
670 #ifdef Q_OS_WINRT
671         d->file.prepend(QStringLiteral("qfile:"));
672 #endif
673     }
674     d->media_changed = url_old != d->file;
675     if (d->media_changed) {
676         d->format_forced.clear();
677     }
678     // a local file. return here to avoid protocol checking. If path contains ":", protocol checking will fail
679     if (d->file.startsWith(QLatin1Char('/')))
680         return d->media_changed;
681     // use MediaIO to support protocols not supported by ffmpeg
682     colon = d->file.indexOf(QLatin1Char(':'));
683     if (colon >= 0) {
684 #ifdef Q_OS_WIN
685         if (colon == 1 && d->file.at(0).isLetter())
686             return d->media_changed;
687 #endif
688         const QString scheme = colon == 0 ? QStringLiteral("qrc") : d->file.left(colon);
689         // supportedProtocols() is not complete. so try MediaIO 1st, if not found, fallback to libavformat
690         d->input = MediaIO::createForProtocol(scheme);
691         if (d->input) {
692             d->input->setUrl(d->file);
693         }
694     }
695     return d->media_changed;
696 }
697 
698 bool AVDemuxer::setMedia(QIODevice* device)
699 {
700     d->file = QString();
701     d->file_orig = QString();
702     if (d->input) {
703         if (d->input->name() != QLatin1String("QIODevice")) {
704             delete d->input;
705             d->input = 0;
706         }
707     }
708     if (!d->input)
709         d->input = MediaIO::create("QIODevice");
710     QIODevice* old_dev = d->input->property("device").value<QIODevice*>();
711     d->media_changed = old_dev != device;
712     if (d->media_changed) {
713         d->format_forced.clear();
714     }
715     d->input->setProperty("device", QVariant::fromValue(device)); //open outside?
716     return d->media_changed;
717 }
718 
719 bool AVDemuxer::setMedia(MediaIO *in)
720 {
721     d->media_changed = in != d->input;
722     if (d->media_changed) {
723         d->format_forced.clear();
724     }
725     d->file = QString();
726     d->file_orig = QString();
727     if (!d->input)
728         d->input = in;
729     if (d->input != in) {
730         delete d->input;
731         d->input = in;
732     }
733     return d->media_changed;
734 }
735 
736 void AVDemuxer::setFormat(const QString &fmt)
737 {
738     d->format_forced = fmt;
739 }
740 
741 QString AVDemuxer::formatForced() const
742 {
743     return d->format_forced;
744 }
745 
746 bool AVDemuxer::load()
747 {
748     unload();
749     qDebug("all closed and reseted");
750 
751     if (d->file.isEmpty() && !d->input) {
752         setMediaStatus(NoMedia);
753         return false;
754     }
755     QMutexLocker lock(&d->mutex); // TODO: load in AVDemuxThread and remove all locks
756     Q_UNUSED(lock);
757     setMediaStatus(LoadingMedia);
758     d->checkNetwork();
759 #if QTAV_HAVE(AVDEVICE)
760     static const QString avd_scheme(QStringLiteral("avdevice:"));
761     if (d->file.startsWith(avd_scheme)) {
762         QStringList parts = d->file.split(QStringLiteral(":"));
763         int s0 = avd_scheme.size();
764         const int s1 = d->file.indexOf(QChar(':'), s0);
765         if (s1 < 0) {
766             qDebug("invalid avdevice specification");
767             setMediaStatus(InvalidMedia);
768             return false;
769         }
770         if (d->file.at(s0) == QChar('/') && d->file.at(s0+1) == QChar('/')) {
771             // avdevice://avfoundation:device_name
772             s0 += 2;
773         } // else avdevice:video4linux2:file_name
774         d->input_format = av_find_input_format(d->file.mid(s0, s1-s0).toUtf8().constData());
775         d->file = d->file.mid(s1+1);
776     }
777 #endif
778     //alloc av format context
779     if (!d->format_ctx)
780         d->format_ctx = avformat_alloc_context();
781     d->format_ctx->flags |= AVFMT_FLAG_GENPTS;
782     //install interrupt callback
783     d->format_ctx->interrupt_callback = *d->interrupt_hanlder;
784 
785     d->applyOptionsForDict();
786     // check special dict keys
787     // d->format_forced can be set from AVFormatContext.format_whitelist
788     if (!d->format_forced.isEmpty()) {
789         d->input_format = av_find_input_format(d->format_forced.toUtf8().constData());
790         qDebug() << "force format: " << d->format_forced;
791     }
792     int ret = 0;
793     // used dict entries will be removed in avformat_open_input
794     d->interrupt_hanlder->begin(InterruptHandler::Open);
795     if (d->input) {
796         if (d->input->accessMode() == MediaIO::Write) {
797             qWarning("wrong MediaIO accessMode. MUST be Read");
798         }
799         d->format_ctx->pb = (AVIOContext*)d->input->avioContext();
800         d->format_ctx->flags |= AVFMT_FLAG_CUSTOM_IO;
801         qDebug("avformat_open_input: d->format_ctx:'%p'..., MediaIO('%s'): %p", d->format_ctx, d->input->name().toUtf8().constData(), d->input);
802         ret = avformat_open_input(&d->format_ctx, "MediaIO", d->input_format, d->options.isEmpty() ? NULL : &d->dict);
803         qDebug("avformat_open_input: (with MediaIO) ret:%d", ret);
804     } else {
805         qDebug("avformat_open_input: d->format_ctx:'%p', url:'%s'...",d->format_ctx, qPrintable(d->file));
806         ret = avformat_open_input(&d->format_ctx, d->file.toUtf8().constData(), d->input_format, d->options.isEmpty() ? NULL : &d->dict);
807         qDebug("avformat_open_input: url:'%s' ret:%d",qPrintable(d->file), ret);
808     }
809     d->interrupt_hanlder->end();
810     if (ret < 0) {
811         // d->format_ctx is 0
812         AVError::ErrorCode ec = AVError::OpenError;
813         QString msg = tr("failed to open media");
814         handleError(ret, &ec, msg);
815         qWarning() << "Can't open media: " << msg;
816         if (mediaStatus() == LoadingMedia) //workaround for timeout but not interrupted
817             setMediaStatus(InvalidMedia);
818         Q_EMIT unloaded(); //context not ready. so will not emit in unload()
819         return false;
820     }
821     //deprecated
822     //if(av_find_stread->inputfo(d->format_ctx)<0) {
823     //TODO: avformat_find_stread->inputfo is too slow, only useful for some video format
824     d->interrupt_hanlder->begin(InterruptHandler::FindStreamInfo);
825     ret = avformat_find_stream_info(d->format_ctx, NULL);
826     d->interrupt_hanlder->end();
827 
828     if (ret < 0) {
829         setMediaStatus(InvalidMedia);
830         AVError::ErrorCode ec(AVError::ParseStreamError);
831         QString msg(tr("failed to find stream info"));
832         handleError(ret, &ec, msg);
833         qWarning() << "Can't find stream info: " << msg;
834         // context is ready. unloaded() will be emitted in unload()
835         if (mediaStatus() == LoadingMedia) //workaround for timeout but not interrupted
836             setMediaStatus(InvalidMedia);
837         return false;
838     }
839 
840     if (!d->prepareStreams()) {
841         if (mediaStatus() == LoadingMedia)
842             setMediaStatus(InvalidMedia);
843         return false;
844     }
845     d->started = false;
846     setMediaStatus(LoadedMedia);
847     Q_EMIT loaded();
848     const bool was_seekable = d->seekable;
849     d->seekable = d->checkSeekable();
850     if (was_seekable != d->seekable)
851         Q_EMIT seekableChanged();
852     qDebug("avfmtctx.flags: %d, iformat.flags", d->format_ctx->flags, d->format_ctx->iformat->flags);
853     if (getInterruptStatus() < 0) {
854         QString msg;
855         qDebug("AVERROR_EXIT: %d", AVERROR_EXIT);
856         handleError(AVERROR_EXIT, 0, msg);
857         qWarning() << "User interupted: " << msg;
858         return false;
859     }
860     return true;
861 }
862 
863 bool AVDemuxer::unload()
864 {
865     QMutexLocker lock(&d->mutex);
866     Q_UNUSED(lock);
867     /*
868     if (d->seekable) {
869         d->seekable = false; //
870         Q_EMIT seekableChanged();
871     }
872     */
873     d->network = false;
874     d->has_attached_pic = false;
875     d->eof = false; // true and set false in load()?
876     d->buf_pos = 0;
877     d->started = false;
878     d->max_pts = 0.0;
879     d->resetStreams();
880     d->interrupt_hanlder->setStatus(0);
881     //av_close_input_file(d->format_ctx); //deprecated
882     if (d->format_ctx) {
883         qDebug("closing d->format_ctx");
884         avformat_close_input(&d->format_ctx); //libavf > 53.10.0
885         d->format_ctx = 0;
886         d->input_format = 0;
887         // no delete. may be used in next load
888         if (d->input)
889             d->input->release();
890         Q_EMIT unloaded();
891     }
892     return true;
893 }
894 
895 bool AVDemuxer::isLoaded() const
896 {
897     return d->format_ctx && (d->astream.avctx || d->vstream.avctx || d->sstream.avctx);
898 }
899 
900 bool AVDemuxer::hasAttacedPicture() const
901 {
902     return d->has_attached_pic;
903 }
904 
905 bool AVDemuxer::setStreamIndex(StreamType st, int index)
906 {
907     QList<int> *streams = 0;
908     Private::StreamInfo *si = 0;
909     if (st == AudioStream) { // TODO: use a struct
910         si = &d->astream;
911         streams = &d->audio_streams;
912     } else if (st == VideoStream) {
913         si = &d->vstream;
914         streams = &d->video_streams;
915     } else if (st == SubtitleStream) {
916         si = &d->sstream;
917         streams = &d->subtitle_streams;
918     }
919     if (!si) {
920         qWarning("stream type %d for index %d not found", st, index);
921         return false;
922     }
923     if (index >= streams->size()) {// || index < 0) { //TODO: disable if <0
924         //si->wanted_stream = -1;
925         qWarning("invalid index %d (valid is 0~%d) for stream type %d.", index, streams->size(), st);
926         return false;
927     }
928     if (index < 0) {
929         qDebug("disable %d stream", st);
930         si->stream = -1;
931         si->wanted_index = -1;
932         si->wanted_stream = -1;
933         return true;
934     }
935     if (!d->setStream(st, streams->at(index)))
936         return false;
937     si->wanted_index = index;
938     return true;
939 }
940 
941 AVFormatContext* AVDemuxer::formatContext()
942 {
943     return d->format_ctx;
944 }
945 
946 QString AVDemuxer::formatName() const
947 {
948     if (!d->format_ctx)
949         return QString();
950     return QLatin1String(d->format_ctx->iformat->name);
951 }
952 
953 QString AVDemuxer::formatLongName() const
954 {
955     if (!d->format_ctx)
956         return QString();
957     return QLatin1String(d->format_ctx->iformat->long_name);
958 }
959 
960 // convert to s using AV_TIME_BASE then *1000?
961 qint64 AVDemuxer::startTime() const
962 {
963     return startTimeUs()/1000LL; //TODO: av_rescale
964 }
965 
966 qint64 AVDemuxer::duration() const
967 {
968     return durationUs()/1000LL; //time base: AV_TIME_BASE TODO: av_rescale
969 }
970 
971 //AVFrameContext use AV_TIME_BASE as time base. AVStream use their own timebase
972 qint64 AVDemuxer::startTimeUs() const
973 {
974     // start time may be not null for network stream
975     if (!d->format_ctx || d->format_ctx->start_time == AV_NOPTS_VALUE)
976         return 0;
977     return d->format_ctx->start_time;
978 }
979 
980 qint64 AVDemuxer::durationUs() const
981 {
982     if (!d->format_ctx || d->format_ctx->duration == AV_NOPTS_VALUE)
983         return 0;
984     return d->format_ctx->duration; //time base: AV_TIME_BASE
985 }
986 
987 int AVDemuxer::bitRate() const
988 {
989     return d->format_ctx->bit_rate;
990 }
991 
992 qreal AVDemuxer::frameRate() const
993 {
994     if (videoStream() < 0)
995         return 0;
996     AVStream *stream = d->format_ctx->streams[videoStream()];
997     return av_q2d(stream->avg_frame_rate);
998     //codecCtx->time_base.den / codecCtx->time_base.num
999 }
1000 
1001 qint64 AVDemuxer::frames(int stream) const
1002 {
1003     if (stream == -1) {
1004         stream = videoStream();
1005         if (stream < 0)
1006             stream = audioStream();
1007         if (stream < 0)
1008             return 0;
1009     }
1010     return d->format_ctx->streams[stream]->nb_frames;
1011 }
1012 
1013 int AVDemuxer::currentStream(StreamType st) const
1014 {
1015     if (st == AudioStream)
1016         return audioStream();
1017     else if (st == VideoStream)
1018         return videoStream();
1019     else if (st == SubtitleStream)
1020         return subtitleStream();
1021     return -1;
1022 }
1023 
1024 QList<int> AVDemuxer::streams(StreamType st) const
1025 {
1026     if (st == AudioStream)
1027         return audioStreams();
1028     else if (st == VideoStream)
1029         return videoStreams();
1030     else if (st == SubtitleStream)
1031         return subtitleStreams();
1032     return QList<int>();
1033 }
1034 
1035 int AVDemuxer::audioStream() const
1036 {
1037     return d->astream.stream;
1038 }
1039 
1040 QList<int> AVDemuxer::audioStreams() const
1041 {
1042     return d->audio_streams;
1043 }
1044 
1045 int AVDemuxer::videoStream() const
1046 {
1047     return d->vstream.stream;
1048 }
1049 
1050 QList<int> AVDemuxer::videoStreams() const
1051 {
1052     return d->video_streams;
1053 }
1054 
1055 int AVDemuxer::subtitleStream() const
1056 {
1057     return d->sstream.stream;
1058 }
1059 
1060 QList<int> AVDemuxer::subtitleStreams() const
1061 {
1062     return d->subtitle_streams;
1063 }
1064 
1065 AVCodecContext* AVDemuxer::audioCodecContext(int stream) const
1066 {
1067     if (stream < 0)
1068         return d->astream.avctx;
1069     if (stream > (int)d->format_ctx->nb_streams)
1070         return 0;
1071     AVCodecContext *avctx = d->format_ctx->streams[stream]->codec;
1072     if (avctx->codec_type == AVMEDIA_TYPE_AUDIO)
1073         return avctx;
1074     return 0;
1075 }
1076 
1077 AVCodecContext* AVDemuxer::videoCodecContext(int stream) const
1078 {
1079     if (stream < 0)
1080         return d->vstream.avctx;
1081     if (stream > (int)d->format_ctx->nb_streams)
1082         return 0;
1083     AVCodecContext *avctx = d->format_ctx->streams[stream]->codec;
1084     if (avctx->codec_type == AVMEDIA_TYPE_VIDEO)
1085         return avctx;
1086     return 0;
1087 }
1088 
1089 AVCodecContext* AVDemuxer::subtitleCodecContext(int stream) const
1090 {
1091     if (stream < 0)
1092         return d->sstream.avctx;
1093     if (stream > (int)d->format_ctx->nb_streams)
1094         return 0;
1095     AVCodecContext *avctx = d->format_ctx->streams[stream]->codec;
1096     if (avctx->codec_type == AVMEDIA_TYPE_SUBTITLE)
1097         return avctx;
1098     return 0;
1099 }
1100 
1101 /**
1102  * @brief getInterruptTimeout return the interrupt timeout
1103  * @return
1104  */
1105 qint64 AVDemuxer::getInterruptTimeout() const
1106 {
1107     return d->interrupt_hanlder->getTimeout();
1108 }
1109 
1110 /**
1111  * @brief setInterruptTimeout set the interrupt timeout
1112  * @param timeout
1113  * @return
1114  */
1115 void AVDemuxer::setInterruptTimeout(qint64 timeout)
1116 {
1117     d->interrupt_hanlder->setTimeout(timeout);
1118 }
1119 
1120 bool AVDemuxer::isInterruptOnTimeout() const
1121 {
1122     return d->interrupt_hanlder->isInterruptOnTimeout();
1123 }
1124 
1125 void AVDemuxer::setInterruptOnTimeout(bool value)
1126 {
1127     d->interrupt_hanlder->setInterruptOnTimeout(value);
1128 }
1129 
1130 int AVDemuxer::getInterruptStatus() const
1131 {
1132     return d->interrupt_hanlder->getStatus();
1133 }
1134 
1135 void AVDemuxer::setInterruptStatus(int interrupt)
1136 {
1137     d->interrupt_hanlder->setStatus(interrupt);
1138 }
1139 
1140 void AVDemuxer::setOptions(const QVariantHash &dict)
1141 {
1142     d->options = dict;
1143     d->applyOptionsForContext(); // apply even if avformat context is open
1144 }
1145 
1146 QVariantHash AVDemuxer::options() const
1147 {
1148     return d->options;
1149 }
1150 
1151 void AVDemuxer::setMediaStatus(MediaStatus status)
1152 {
1153     if (d->media_status == status)
1154         return;
1155 
1156     //if (status == NoMedia || status == InvalidMedia)
1157     //    Q_EMIT durationChanged(0);
1158 
1159     d->media_status = status;
1160 
1161     Q_EMIT mediaStatusChanged(d->media_status);
1162 }
1163 
1164 void AVDemuxer::Private::applyOptionsForDict()
1165 {
1166     if (dict) {
1167         av_dict_free(&dict);
1168         dict = 0; //aready 0 in av_free
1169     }
1170     if (options.isEmpty())
1171         return;
1172     QVariant opt(options);
1173     if (options.contains(QStringLiteral("avformat")))
1174         opt = options.value(QStringLiteral("avformat"));
1175     Internal::setOptionsToDict(opt, &dict);
1176 
1177     if (opt.type() == QVariant::Map) {
1178         QVariantMap avformat_dict(opt.toMap());
1179         if (avformat_dict.contains(QStringLiteral("format_whitelist"))) {
1180             const QString fmts(avformat_dict[QStringLiteral("format_whitelist")].toString());
1181             if (!fmts.contains(QLatin1Char(',')) && !fmts.isEmpty())
1182                 format_forced = fmts; // reset when media changed
1183         }
1184     } else if (opt.type() == QVariant::Hash) {
1185         QVariantHash avformat_dict(opt.toHash());
1186         if (avformat_dict.contains(QStringLiteral("format_whitelist"))) {
1187             const QString fmts(avformat_dict[QStringLiteral("format_whitelist")].toString());
1188             if (!fmts.contains(QLatin1Char(',')) && !fmts.isEmpty())
1189                 format_forced = fmts; // reset when media changed
1190         }
1191     }
1192 }
1193 
1194 void AVDemuxer::Private::applyOptionsForContext()
1195 {
1196     if (!format_ctx)
1197         return;
1198     if (options.isEmpty()) {
1199         //av_opt_set_defaults(format_ctx);  //can't set default values! result maybe unexpected
1200         return;
1201     }
1202     QVariant opt(options);
1203     if (options.contains(QStringLiteral("avformat")))
1204         opt = options.value(QStringLiteral("avformat"));
1205     Internal::setOptionsToFFmpegObj(opt, format_ctx);
1206 }
1207 
1208 void AVDemuxer::handleError(int averr, AVError::ErrorCode *errorCode, QString &msg)
1209 {
1210     if (averr >= 0)
1211         return;
1212     // d->format_ctx is 0
1213     // TODO: why sometimes AVERROR_EXIT does not work?
1214     bool interrupted = (averr == AVERROR_EXIT) || getInterruptStatus();
1215     QString err_msg(msg);
1216     if (interrupted) { // interrupted by callback, so can not determine whether the media is valid
1217         // insufficient buffering or other interruptions
1218         if (getInterruptStatus() < 0) {
1219             setMediaStatus(StalledMedia);
1220             Q_EMIT userInterrupted();
1221             err_msg += QStringLiteral(" [%1]").arg(tr("interrupted by user"));
1222         } else {
1223             // FIXME: if not interupt on timeout and ffmpeg exits, still LoadingMedia
1224             if (isInterruptOnTimeout())
1225                 setMediaStatus(StalledMedia);
1226             // averr is eof for open timeout
1227             err_msg += QStringLiteral(" [%1]").arg(tr("timeout"));
1228         }
1229     } else {
1230         if (mediaStatus() == LoadingMedia)
1231             setMediaStatus(InvalidMedia);
1232     }
1233     msg = err_msg;
1234     if (!errorCode)
1235         return;
1236     AVError::ErrorCode ec(*errorCode);
1237     if (averr == AVERROR_INVALIDDATA) { // leave it if reading
1238         if (*errorCode == AVError::OpenError)
1239             ec = AVError::FormatError;
1240     } else {
1241         // Input/output error etc.
1242         if (d->network)
1243             ec = AVError::NetworkError;
1244     }
1245     AVError err(ec, err_msg, averr);
1246     Q_EMIT error(err);
1247     *errorCode = ec;
1248 }
1249 
1250 bool AVDemuxer::Private::setStream(AVDemuxer::StreamType st, int streamValue)
1251 {
1252     if (streamValue < -1)
1253         streamValue = -1;
1254     QList<int> *streams = 0;
1255     Private::StreamInfo *si = 0;
1256     if (st == AudioStream) { // TODO: use a struct
1257         si = &astream;
1258         streams = &audio_streams;
1259     } else if (st == VideoStream) {
1260         si = &vstream;
1261         streams = &video_streams;
1262     } else if (st == SubtitleStream) {
1263         si = &sstream;
1264         streams = &subtitle_streams;
1265     }
1266     if (!si /*|| si->wanted_stream == streamValue*/) { //init -2
1267         qWarning("stream type %d not found", st);
1268         return false;
1269     }
1270     //if (!streams->contains(si->stream)) {
1271       //  qWarning("%d is not a valid stream for stream type %d", si->stream, st);
1272         //return false;
1273     //}
1274     bool index_valid = si->wanted_index >= 0 && si->wanted_index < streams->size();
1275     int s = AVERROR_STREAM_NOT_FOUND;
1276     if (streamValue >= 0 || !index_valid) {
1277         // or simply set s to streamValue if value is contained in streams?
1278         s = av_find_best_stream(format_ctx
1279                                 , st == AudioStream ? AVMEDIA_TYPE_AUDIO
1280                                 : st == VideoStream ? AVMEDIA_TYPE_VIDEO
1281                                 : st == SubtitleStream ? AVMEDIA_TYPE_SUBTITLE
1282                                 : AVMEDIA_TYPE_UNKNOWN
1283                                 , streamValue, -1, NULL, 0); // streamValue -1 is ok
1284     } else { //index_valid
1285         s = streams->at(si->wanted_index);
1286     }
1287     if (s == AVERROR_STREAM_NOT_FOUND)
1288         return false;
1289     // don't touch wanted index
1290     si->stream = s;
1291     si->wanted_stream = streamValue;
1292     si->avctx = format_ctx->streams[s]->codec;
1293     has_attached_pic = !!(format_ctx->streams[s]->disposition & AV_DISPOSITION_ATTACHED_PIC);
1294     return true;
1295 }
1296 
1297 bool AVDemuxer::Private::prepareStreams()
1298 {
1299     has_attached_pic = false;
1300     resetStreams();
1301     if (!format_ctx)
1302         return false;
1303     AVMediaType type = AVMEDIA_TYPE_UNKNOWN;
1304     for (unsigned int i = 0; i < format_ctx->nb_streams; ++i) {
1305         type = format_ctx->streams[i]->codec->codec_type;
1306         if (type == AVMEDIA_TYPE_VIDEO) {
1307             video_streams.push_back(i);
1308         } else if (type == AVMEDIA_TYPE_AUDIO) {
1309             audio_streams.push_back(i);
1310         } else if (type == AVMEDIA_TYPE_SUBTITLE) {
1311             subtitle_streams.push_back(i);
1312         }
1313     }
1314     if (audio_streams.isEmpty() && video_streams.isEmpty() && subtitle_streams.isEmpty())
1315         return false;
1316     setStream(AVDemuxer::AudioStream, -1);
1317     setStream(AVDemuxer::VideoStream, -1);
1318     setStream(AVDemuxer::SubtitleStream, -1);
1319     return true;
1320 }
1321 } //namespace QtAV
1322