root/libmedia/ffmpeg/MediaParserFfmpeg.cpp

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. as_double
  2. readPacketWrapper
  3. seekMediaWrapper
  4. probeStream
  5. seek
  6. parseVideoFrame
  7. parseAudioFrame
  8. parseNextFrame
  9. parseNextChunk
  10. getBytesLoaded
  11. _lastParsedPosition
  12. initializeParser
  13. readPacket
  14. seekMedia
  15. SampleFormatToSampleSize

// MediaParserFfmpeg.cpp: FFMPEG media parsers, for Gnash
//
//   Copyright (C) 2007, 2008, 2009, 2010, 2011 Free Software Foundation, Inc.
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
//

#include "ffmpegHeaders.h"
#include "MediaParserFfmpeg.h"
#include "GnashException.h"
#include "log.h"
#include "IOChannel.h"

//#define GNASH_ALLOW_VCODEC_ENV 1
// Set this to enable a special GNASH_DEFAULT_VCODEC environment variable, which
// is used as a default when the video codec can't be detected. This is a quick
// hack to make MJPEG HTTP videos work (which can't be detected as their MIME
// type is just "mixed/multipart"). Perhaps the codec will be configurable via
// ActionScript sometime. - Udo 

namespace gnash {
namespace media {
namespace ffmpeg {

namespace { 

        // Used to calculate a decimal value from a ffmpeg fraction
        inline double as_double(AVRational time)
        {
                return time.num / static_cast<double>(time.den);
        }

} // anonymous namespace


int
MediaParserFfmpeg::readPacketWrapper(void* opaque, boost::uint8_t* buf, int buf_size)
{
        MediaParserFfmpeg* p = static_cast<MediaParserFfmpeg*>(opaque);
        return p->readPacket(buf, buf_size);
}

boost::int64_t
MediaParserFfmpeg::seekMediaWrapper(void *opaque, boost::int64_t offset, int whence)
{
        MediaParserFfmpeg* p = static_cast<MediaParserFfmpeg*>(opaque);
        return p->seekMedia(offset, whence);
}

AVInputFormat*
MediaParserFfmpeg::probeStream()
{
    const size_t probeSize = 2048;
    const size_t bufSize = probeSize + FF_INPUT_BUFFER_PADDING_SIZE;

        boost::scoped_array<boost::uint8_t> buffer(new boost::uint8_t[bufSize]);

        assert(_stream->tell() == static_cast<std::streampos>(0));
        size_t actuallyRead = _stream->read(buffer.get(), probeSize);
    
    // Fill any padding with 0s.
    std::fill(buffer.get() + actuallyRead, buffer.get() + bufSize, 0);

        _stream->seek(0);

        if (actuallyRead < 1)
        {
                throw IOException(_("MediaParserFfmpeg could not read probe data "
                    "from input"));
        }

        // Probe the file to detect the format
        AVProbeData probe_data;
        probe_data.filename = "";
        probe_data.buf = buffer.get();
    probe_data.buf_size = actuallyRead;
        
    AVInputFormat* ret = av_probe_input_format(&probe_data, 1);
        return ret;
}

bool
MediaParserFfmpeg::seek(boost::uint32_t& pos)
{
        LOG_ONCE(log_unimpl("MediaParserFfmpeg::seek()"));
        return false;

        log_debug("MediaParserFfmpeg::seek(%d) TESTING", pos);

        AVStream* videostream = _formatCtx->streams[_videoStreamIndex];
        double timebase = static_cast<double>(videostream->time_base.num / videostream->time_base.den);
        long newpos = static_cast<long>(pos / timebase);
                
        if (av_seek_frame(_formatCtx, _videoStreamIndex, newpos, 0) < 0)
        {
                log_error(_("%s: seeking failed"), __FUNCTION__);
                return 0;
        }

        AVPacket Packet;
        av_init_packet(&Packet);
        double newtime = 0;
        while (newtime == 0)
        {
                if (av_read_frame(_formatCtx, &Packet) < 0) 
                {
                        log_error("Error in av_read_frame (while seeking)");
                        av_seek_frame(_formatCtx, -1, 0, AVSEEK_FLAG_BACKWARD);
                        //av_free_packet( &Packet );
                        return 0; // ??
                }

                newtime = timebase * static_cast<double>(_formatCtx->streams[_videoStreamIndex]->cur_dts);
        }

        //av_free_packet( &Packet );
        av_seek_frame(_formatCtx, _videoStreamIndex, newpos, 0);

        newtime = static_cast<boost::int32_t>(newtime / 1000.0);
        log_debug("Seek requested to time %d triggered seek to key frame at "
            "time %d", pos, newtime);
        pos = newtime;

    return true;
}

bool
MediaParserFfmpeg::parseVideoFrame(AVPacket& packet)
{
        assert(packet.stream_index == _videoStreamIndex);
        assert(_videoStream);

        // packet.dts is "decompression" timestamp
        // packet.pts is "presentation" timestamp
        // Dunno why we use dts, and don't understand the magic formula either...
        //
        // From ffmpeg dox:
        //    pkt->pts can be AV_NOPTS_VALUE if the video format has B frames,
        //    so it is better to rely on pkt->dts if you do not decompress the payload.
        //
        boost::uint64_t timestamp = static_cast<boost::uint64_t>(packet.dts * as_double(_videoStream->time_base) * 1000.0); 

#if 0
        LOG_ONCE( log_unimpl("%s", __PRETTY_FUNCTION__) );
        return false;
#else

        // flags, for keyframe
        //bool isKeyFrame = packet.flags&PKT_FLAG_KEY;

        // TODO: FIXME: *2 is an hack to avoid libavcodec reading past end of allocated space
        //       we might do proper padding or (better) avoid the copy as a whole by making
        //       EncodedVideoFrame virtual.
        size_t allocSize = packet.size*2;
        boost::uint8_t* data = new boost::uint8_t[allocSize];
        std::copy(packet.data, packet.data+packet.size, data);
        std::auto_ptr<EncodedVideoFrame> frame(new EncodedVideoFrame(data, packet.size, 0, timestamp));

        pushEncodedVideoFrame(frame);

        return true;
#endif
}

bool
MediaParserFfmpeg::parseAudioFrame(AVPacket& packet)
{
        assert(packet.stream_index == _audioStreamIndex);
        assert(_audioStream);

        // packet.dts is "decompression" timestamp
        // packet.pts is "presentation" timestamp
        // Dunno why we use dts, and don't understand the magic formula either...
        //
        // From ffmpeg dox:
        //    pkt->pts can be AV_NOPTS_VALUE if the video format has B frames,
        //    so it is better to rely on pkt->dts if you do not decompress the payload.
        //

        boost::uint64_t dts = packet.dts;
    if ( dts == static_cast<boost::uint64_t>(AV_NOPTS_VALUE) ) {
        // We'll take 'nopts' value as zero.
        // Would likely be better to make it use timestamp
        // of previous frame, if any.
        //
        // For now, this handling fixes warnings like:
        //   mdb:93, lastbuf:0 skiping granule 0
        //   mdb:93, lastbuf:0 skiping granule 0
        // When playing: http://downloads.bbc.co.uk/news/nol/shared/spl/hi/audio_slideshow/kenadamptw/slideshow_629.swf
        //
        LOG_ONCE(log_error("FIXME: FFmpeg packet decompression "
                    "timestamp has no value, taking as zero"));
        dts = 0;
    }
        boost::uint64_t timestamp = static_cast<boost::uint64_t>(dts * as_double(_audioStream->time_base) * 1000.0); 
    //log_debug("On getting audio frame with timestamp %d, duration is %d", timestamp, _audioStream->duration);

        std::auto_ptr<EncodedAudioFrame> frame ( new EncodedAudioFrame );

        // TODO: FIXME: *2 is an hack to avoid libavcodec reading past end of allocated space
        //       we might do proper padding or (better) avoid the copy as a whole by making
        //       EncodedVideoFrame virtual.
        size_t allocSize = packet.size*2;
        boost::uint8_t* data = new boost::uint8_t[allocSize];
        std::copy(packet.data, packet.data+packet.size, data);

        frame->data.reset(data); 
        frame->dataSize = packet.size;
        frame->timestamp = timestamp;

        pushEncodedAudioFrame(frame); 

        return true;
}

bool
MediaParserFfmpeg::parseNextFrame()
{
        // lock the stream while reading from it, so actionscript
        // won't mess with the parser on seek  or on getBytesLoaded
        boost::mutex::scoped_lock streamLock(_streamMutex);

        if ( _parsingComplete )
        {
                //log_debug("MediaParserFfmpeg::parseNextFrame: parsing "
        //"complete, nothing to do");
                return false;
        }

        // position the stream where we left parsing as
        // it could be somewhere else for reading a specific
        // or seeking.
        //_stream->seek(_lastParsedPosition);

        assert(_formatCtx);

        AVPacket packet;

        //log_debug("av_read_frame call");
        int rc = av_read_frame(_formatCtx, &packet);

        // Update _lastParsedPosition, even in case of error..
        boost::uint64_t curPos = _stream->tell();
        if ( curPos > _lastParsedPosition )
        {
                _lastParsedPosition = curPos;
        }

        //log_debug("av_read_frame returned %d", rc);
        if ( rc < 0 )
        {
        log_error(_("MediaParserFfmpeg::parseNextFrame: "
            "Problems parsing next frame "
            "(av_read_frame returned %d)."
            " We'll consider the stream fully parsed."), rc);
        _parsingComplete=true; // No point in parsing over
        return false;
        }

        bool ret = false;

        if ( packet.stream_index == _videoStreamIndex )
        {
                ret = parseVideoFrame(packet);
        }
        else if ( packet.stream_index == _audioStreamIndex )
        {
                ret = parseAudioFrame(packet);
        }
        else
        {
                ret = false; // redundant..
                log_debug("MediaParserFfmpeg::parseNextFrame: unknown stream index %d",
                packet.stream_index);
        }

        av_free_packet(&packet);

        // Check if EOF was reached
        if ( _stream->eof() )
        {
                log_debug("MediaParserFfmpeg::parseNextFrame: at eof after "
                "av_read_frame");
                _parsingComplete=true;
        }

        return ret;
}

bool
MediaParserFfmpeg::parseNextChunk()
{
        if ( ! parseNextFrame() ) return false;
        return true;
}

boost::uint64_t
MediaParserFfmpeg::getBytesLoaded() const
{
        //log_unimpl("%s", __PRETTY_FUNCTION__);
        return _lastParsedPosition;
}

MediaParserFfmpeg::MediaParserFfmpeg(std::auto_ptr<IOChannel> stream)
        :
        MediaParser(stream),
        _nextVideoFrame(0),
        _nextAudioFrame(0),
        _inputFmt(0),
        _formatCtx(0),
        _videoStreamIndex(-1),
        _videoStream(0),
        _audioStreamIndex(-1),
        _audioStream(0),
        _lastParsedPosition(0)
{
        initializeParser();

        startParserThread();
}

/*private*/
void
MediaParserFfmpeg::initializeParser()
{
    av_register_all(); // TODO: needs to be invoked only once ?

    _byteIOCxt.buffer = NULL;
    
    _inputFmt = probeStream();
#ifdef GNASH_ALLOW_VCODEC_ENV   
    if ( ! _inputFmt ) {
        char* defcodec = getenv("GNASH_DEFAULT_VCODEC");
        if (defcodec && strlen(defcodec))
            _inputFmt = av_find_input_format(defcodec); 
        
    }
#endif  
    if ( ! _inputFmt ) {
        throw MediaException("MediaParserFfmpeg couldn't figure out input "
                             "format");
    }
    
// av_alloc_format_context was deprecated on
// 2009-02-08 (r17047) in favor of avformat_alloc_context() 
#if !defined (LIBAVCODEC_VERSION_MAJOR) || LIBAVCODEC_VERSION_MAJOR < 52
    _formatCtx = av_alloc_format_context();
#else
    _formatCtx = avformat_alloc_context();
#endif

    assert(_formatCtx);
    
    // Setup the filereader/seeker mechanism.
    // 7th argument (NULL) is the writer function,
    // which isn't needed.
    _byteIOBuffer.reset( new unsigned char[byteIOBufferSize] );
    init_put_byte(&_byteIOCxt,
                  _byteIOBuffer.get(), // buffer
                  byteIOBufferSize, // buffer size
                  0, // write flags
                  this, // opaque pointer to pass to the callbacks
                  MediaParserFfmpeg::readPacketWrapper, // packet reader callback
                  NULL, // packet writer callback
                  MediaParserFfmpeg::seekMediaWrapper // seeker callback
                  );
    
    _byteIOCxt.is_streamed = 1;
    
    // Open the stream. the 4th argument is the filename, which we ignore.
    if(av_open_input_stream(&_formatCtx, &_byteIOCxt, "", _inputFmt, NULL) < 0)
    {
        throw IOException("MediaParserFfmpeg couldn't open input stream");
    }
    
    log_debug("Parsing FFMPEG media file: format:%s; nstreams:%d",
        _inputFmt->name, _formatCtx->nb_streams);
    
    if ( _formatCtx->title[0] )
        log_debug(_("  Title:'%s'"), _formatCtx->title);
    if ( _formatCtx->author[0] )
        log_debug(_("  Author:'%s'"), _formatCtx->author);
    if ( _formatCtx->copyright[0] )
        log_debug(_("  Copyright:'%s'"), _formatCtx->copyright);
    if ( _formatCtx->comment[0] )
        log_debug(_("  Comment:'%s'"), _formatCtx->comment);
    if ( _formatCtx->album[0] )
        log_debug(_("  Album:'%s'"), _formatCtx->album);
    
    // Find first audio and video stream
    for (unsigned int i = 0; i < static_cast<unsigned int>(_formatCtx->nb_streams); i++)
        {
            AVStream* stream = _formatCtx->streams[i];
            if ( ! stream ) {
                log_debug("Stream %d of FFMPEG media file is null ?", i);
                continue;
            }
            
            AVCodecContext* enc = stream->codec; 
            if ( ! enc ) {
                log_debug("Stream %d of FFMPEG media file has no codec info", i);
                continue;
            }
            
            switch (enc->codec_type) {
            case CODEC_TYPE_AUDIO:
                if (_audioStreamIndex < 0) {
                    _audioStreamIndex = i;
                    _audioStream = _formatCtx->streams[i];
                    log_debug(_("  Using stream %d for audio: codec id %d"),
                              i, _audioStream->codec->codec_id);
                    // codec_name will only be filled by avcodec_find_decoder (later);
                }
                break;
                
            case CODEC_TYPE_VIDEO:
                if (_videoStreamIndex < 0) {
                    _videoStreamIndex = i;
                    _videoStream = _formatCtx->streams[i];
                    log_debug(_("  Using stream %d for video: codec id %d"),
                              i, _videoStream->codec->codec_id);
                    // codec_name will only be filled by avcodec_find_decoder (later);
                }
                break;
            default:
                break;
            }
        }
    
    // Create VideoInfo
    if ( _videoStream) {
        const int codec = static_cast<int>(_videoStream->codec->codec_id); 
        boost::uint16_t width = _videoStream->codec->width;
        boost::uint16_t height = _videoStream->codec->height;
        boost::uint16_t frameRate = static_cast<boost::uint16_t>(as_double(_videoStream->r_frame_rate));
#if !defined(HAVE_LIBAVFORMAT_AVFORMAT_H) && !defined(HAVE_FFMPEG_AVCODEC_H)
        boost::uint64_t duration = _videoStream->codec_info_duration;
#else
        boost::uint64_t duration = _videoStream->duration;
#endif
        if (duration == AV_NOPTS_VALUE) {
            log_error("Duration of video stream unknown");
            duration=0; // TODO: guess!
        } else {
            duration = duration / as_double(_videoStream->time_base); // TODO: check this
        }
        
        _videoInfo.reset(new VideoInfo(codec, width, height, frameRate,
                    duration, CODEC_TYPE_CUSTOM /*codec type*/));
        
        // NOTE: AVCodecContext.extradata : void* for 51.11.0, uint8_t* for 51.38.0
        _videoInfo->extra.reset(new ExtraVideoInfoFfmpeg(
                     (uint8_t*)_videoStream->codec->extradata,
                     _videoStream->codec->extradata_size));
        
    }
    
    // Create AudioInfo
    if ( _audioStream) {
        const int codec = static_cast<int>(_audioStream->codec->codec_id); 
        boost::uint16_t sampleRate = _audioStream->codec->sample_rate;
        boost::uint16_t sampleSize = SampleFormatToSampleSize(_audioStream->codec->sample_fmt);
        bool stereo = (_audioStream->codec->channels == 2);
#if !defined(HAVE_LIBAVFORMAT_AVFORMAT_H) && !defined(HAVE_FFMPEG_AVCODEC_H)
        boost::uint64_t duration = _audioStream->codec_info_duration;
#else
        boost::uint64_t duration = _audioStream->duration;
#endif
        if (duration == AV_NOPTS_VALUE) {
            log_error("Duration of audio stream unknown to ffmpeg");
            duration=0; // TODO: guess!
        } 
        else {
            duration = duration / as_double(_audioStream->time_base); // TODO: check this
        }
        
        _audioInfo.reset(new AudioInfo(codec, sampleRate, sampleSize, stereo,
                    duration, CODEC_TYPE_CUSTOM /*codec type*/));
        
        // NOTE: AVCodecContext.extradata : void* for 51.11.0, uint8_t* for 51.38.0
        _audioInfo->extra.reset(new ExtraAudioInfoFfmpeg(
                     (uint8_t*)_audioStream->codec->extradata,
                     _audioStream->codec->extradata_size));
        
    }
    
    
}
    
MediaParserFfmpeg::~MediaParserFfmpeg()
{
        stopParserThread();

        if ( _formatCtx )
        {
                // TODO: check if this is correct (should we create RIIA classes for ffmpeg stuff?)
                //av_close_input_file(_formatCtx); // NOTE: this one triggers a mismatched free/delete on _byteIOBuffer with libavformat.so.52 !
                av_free(_formatCtx);
        }

        if ( _inputFmt )
        {
                // TODO: check if this is correct (should we create RIIA classes for ffmpeg stuff?)
                //av_free(_inputFmt); // it seems this one blows up, could be due to av_free(_formatCtx) above
        }

}

// NOTE: as this function is used as a callback from FFMPEG, it should not
// throw any exceptions, because:
// a) The behaviour of C++ exceptions passed into C code is undefined.
// b) Even if we don't crash and burn, the FFMPEG parser is left in an
//    undefined state.
int 
MediaParserFfmpeg::readPacket(boost::uint8_t* buf, int buf_size)
{
        //GNASH_REPORT_FUNCTION;
        //log_debug("readPacket(%d)", buf_size);

        size_t ret = _stream->read(static_cast<void*>(buf), buf_size);

        return ret;

}

// NOTE: as this function is used as a callback from FFMPEG, it should not
// throw any exceptions, because:
// a) The behaviour of C++ exceptions passed into C code is undefined.
// b) Even if we don't crash and burn, the FFMPEG parser is left in an
//    undefined state.
boost::int64_t 
MediaParserFfmpeg::seekMedia(boost::int64_t offset, int whence)
{
        //GNASH_REPORT_FUNCTION;
        //log_debug("::seekMedia(%1%, %2%)", offset, whence);

        assert(_stream.get());

        if (whence == SEEK_SET)
        {       
                // Offset is absolute new position in the file
                if ( offset < 0 ) {
                        boost::format fmt = boost::format(
                                _("MediaParserFfmpeg couldn't parse input format: "
                                "tried to seek at negative offset %1%."))
                                % offset;
                        log_error(fmt);
                        return -1;
                } else {
                        _stream->seek(offset);
                }
        }
        else if (whence == SEEK_CUR)
        {
                // New position is offset + old position
                _stream->seek(_stream->tell() + static_cast<std::streamoff>(offset));
        }
        else if (whence == SEEK_END)
        {
                // New position is offset + end of file
                log_unimpl("MediaParserFfmpeg seek from end of file");
                // This is (most likely) a streamed file, so we can't seek to the end!
                // Instead we seek to byteIOBufferSize bytes... seems to work fine...
                _stream->seek(byteIOBufferSize);

        }
        else
        {
                // ffmpeg uses whence=AVSEEK_SIZE and offset=0 to request
                // stream size !
                log_unimpl("MediaParserFfmpeg: unsupported whence value %d", whence);
                return -1;
        }


        return _stream->tell(); 
}

boost::uint16_t
MediaParserFfmpeg::SampleFormatToSampleSize(SampleFormat fmt)
{
        switch (fmt)
        {
                case SAMPLE_FMT_U8: // unsigned 8 bits
                        return 1;

                case SAMPLE_FMT_S16: // signed 16 bits
                case SAMPLE_FMT_FLT: // float
                        return 2;

#if !defined (LIBAVCODEC_VERSION_MAJOR) || LIBAVCODEC_VERSION_MAJOR < 52
// Was dropped for version 52.0.0
                case SAMPLE_FMT_S24: // signed 24 bits
                        return 3;
#endif

                case SAMPLE_FMT_S32: // signed 32 bits
                        return 4;

                case SAMPLE_FMT_NONE:
                default:
                        return 8; // arbitrary value
        }
}


} // gnash.media.ffmpeg namespace 
} // end of gnash::media namespace
} // end of gnash namespace

#undef PADDING_BYTES
#undef READ_CHUNKS 

/* [<][>][^][v][top][bottom][index][help] */