diff --git a/avpy/av.py b/avpy/av.py index a0f4387..313b230 100644 --- a/avpy/av.py +++ b/avpy/av.py @@ -128,7 +128,7 @@ def _findModuleName(): intv = versionDict[major] libName = None - for k in intv.keys(): + for k in list(intv.keys()): if minor >= k[0] and minor < k[1]: libName = intv[k] break diff --git a/avpy/av.py.bak b/avpy/av.py.bak new file mode 100644 index 0000000..a0f4387 --- /dev/null +++ b/avpy/av.py.bak @@ -0,0 +1,148 @@ +# Avpy +# Copyright (C) 2013-2015 Sylvain Delhomme +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 +# USA + +import os +import re +from ctypes import CDLL, RTLD_GLOBAL, util + +def _version(): + + ''' + Return libavcodec version as a tuple: lib (libav or ffmpeg), major, minor + and patch version + ''' + + # find_library does not support LD_LIBRARY_PATH for python < 3.4 + if 'AVPY_AVCODEC' in os.environ: + fold, base = os.path.split(os.environ['AVPY_AVCODEC']) + + if 'AVPY_AVUTIL' in os.environ: + libavutil = os.environ['AVPY_AVUTIL'] + else: + libavutil = os.path.join(fold, re.sub('avcodec', 'avutil', base)) + + if 'AVPY_AVRESAMPLE' in os.environ: + libavresample = os.environ['AVPY_AVRESAMPLE'] + else: + libavresample = os.path.join(fold, re.sub('avcodec', 'avresample', base)) + + if 'AVPY_SWRESAMPLE' in os.environ: + libswresample = os.environ['AVPY_SWRESAMPLE'] + else: + libswresample = os.path.join(fold, re.sub('avcodec', 'swresample', base)) + + libavcodec = os.environ['AVPY_AVCODEC'] + else: + libavutil = util.find_library('avutil') + libswresample = util.find_library('swresample') + libavresample = util.find_library('avresample') + libavcodec = util.find_library('avcodec') + + # RTD tests (debug only) + #libavutil = None + #libavcodec = None + #libswresample = None + #libavresample = None + + CDLL(libavutil, RTLD_GLOBAL) + + # ffmpeg have both libswresample and libavresample + # libav only have libavresample + if libswresample and os.path.exists(libswresample): + lib = 'ffmpeg' + else: + lib = 'libav' + + # libav11 + if libavresample and os.path.exists(libavresample): + CDLL(libavresample, RTLD_GLOBAL) + + version = CDLL(libavcodec, mode=RTLD_GLOBAL).avcodec_version() + + return lib, version >> 16 & 0xFF, version >> 8 & 0xFF, version & 0xFF + + +def _findModuleName(): + + ''' + find libav python binding to import + + on error, raise an ImportError exception + ''' + + libavVersion = { + 53: { + #(0, 10): 'av7', + (30, 40): 'av8', + }, + 54: { + (30, 40): 'av9', + }, + 55: { + (30, 40): 'av10', + }, + 56: { + (1, 40): 'av11', + } + } + + ffmpegVersion = { + 54: { + (90, 93): 'ff12', + }, + 56: { + (13, 20): 'ff25', + (26, 30): 'ff26', + (40, 50): 'ff27', + (60, 65): 'ff28', + }, + # 57: { + # (24, 30): 'ff30', + # } + } + + lib, major, minor, micro = _version() + + if lib == 'ffmpeg': + versionDict = ffmpegVersion + else: + versionDict = libavVersion + + if major in versionDict: + + intv = versionDict[major] + libName = None + + for k in intv.keys(): + if minor >= k[0] and minor < k[1]: + libName = intv[k] + break + + if libName is None: + raise ImportError('ffmpeg/libav minor version not supported') + else: + raise ImportError('ffmpeg/libav major version not supported') + + return libName + +_moduleName = _findModuleName() +# import module +_temp = __import__('avpy.version', globals(), locals(), [_moduleName]) +# import as lib +lib = getattr(_temp, _moduleName) + diff --git a/avpy/avMedia.py b/avpy/avMedia.py index 2180496..0e6d6b8 100644 --- a/avpy/avMedia.py +++ b/avpy/avMedia.py @@ -296,9 +296,9 @@ def __iter__(self): def __next__(self): # python3 require __next__, python2 next - return self.next() + return next(self) - def next(self): + def __next__(self): ''' Iterate over Media @@ -492,7 +492,7 @@ def addStream(self, streamType, streamInfo): if res == 0: raise RuntimeError('Codec %s not supported in %s muxer' % errorTpl ) elif res < 0: - print('Warning: could not determine if codec %s is supported by %s muxer' % errorTpl) + print(('Warning: could not determine if codec %s is supported by %s muxer' % errorTpl)) stream = av.lib.avformat_new_stream(self.pFormatCtx, _codec) if not stream: @@ -603,7 +603,7 @@ def addStream(self, streamType, streamInfo): if res == 0: raise RuntimeError('Codec %s not supported in %s muxer' % errorTpl ) elif res < 0: - print('Warning: could not determine if codec %s is supported by %s muxer' % errorTpl) + print(('Warning: could not determine if codec %s is supported by %s muxer' % errorTpl)) stream = av.lib.avformat_new_stream(self.pFormatCtx, _codec) if not stream: @@ -1017,7 +1017,7 @@ def _addScaler(self, streamIndex): scalingId = scaler[4] codecCtx = self.codecCtx[streamIndex] - print(scalingId, av.lib.SWS_BILINEAR) + print((scalingId, av.lib.SWS_BILINEAR)) #raise self.swsCtx[streamIndex] = av.lib.sws_getContext( @@ -1388,7 +1388,7 @@ def decode(self): self.subtitleCount = self.subtitle.num_rects self.subtitleTypes = [] - for i in xrange(self.subtitleCount): + for i in range(self.subtitleCount): cType = self.subtitle.rects[i].contents.type cTypeName = '' diff --git a/avpy/avMedia.py.bak b/avpy/avMedia.py.bak new file mode 100644 index 0000000..2180496 --- /dev/null +++ b/avpy/avMedia.py.bak @@ -0,0 +1,1696 @@ +# Avpy +# Copyright (C) 2013-2015 Sylvain Delhomme +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 +# USA + +import os +import sys +import array +import ctypes +import contextlib + +from . import av +from .avUtil import toString, toCString, _guessScaling, _guessChannelLayout + +__version__ = '0.1.3' + +FRAME_SIZE_DEFAULT = 1152 +FPS_DEFAULT = (1, 24) +PY3 = True if sys.version_info >= (3, 0) else False +AVPY_LIB_NAME = av.lib._libraries['name'] +AVPY_LIB_VERSION = av.lib._libraries['version'] +AVPY_RESAMPLE_SUPPORT = not (AVPY_LIB_NAME == 'libav' and AVPY_LIB_VERSION == 0.8) + +class Media(object): + + def __init__(self, mediaName, mode='r', quiet=True): + + ''' Initialize a media object for decoding or encoding + + :param mediaName: media to open + :type label: str + :param mode: 'r' (decoding) or 'w' (encoding) + :type mode: str + :param quiet: turn on/off libav or ffmpeg warnings + :type quiet: bool + ''' + + if quiet: + av.lib.av_log_set_level(av.lib.AV_LOG_QUIET) + + av.lib.av_register_all() + self.pFormatCtx = ctypes.POINTER(av.lib.AVFormatContext)() + + if mode == 'r': + + # prevent coredump + if mediaName is None: + mediaName = '' + + # open media for reading + + # XXX: ok for python3 and python2 with special characters + # not sure this is a right/elegant solution + if sys.version_info >= (3, 0): + _mediaName = mediaName.encode('utf-8') + else: + _mediaName = mediaName + + res = av.lib.avformat_open_input(self.pFormatCtx, _mediaName, None, None) + if res: + raise IOError(avError(res)) + + # get stream info + # need this call in order to retrieve duration + res = av.lib.avformat_find_stream_info(self.pFormatCtx, None) + if res < 0: + raise IOError(avError(res)) + + elif mode == 'w': + + # FIXME: code dup... + + # XXX: ok for python3 and python2 with special characters + # not sure this is a right/elegant solution + if sys.version_info >= (3, 0): + _mediaName = mediaName.encode('utf-8') + defaultCodec = ctypes.c_char_p('mpeg'.encode('utf8')) + else: + _mediaName = mediaName + defaultCodec = ctypes.c_char_p('mpeg') + + # Autodetect the output format from the name, default to MPEG + fmt = av.lib.av_guess_format(None, _mediaName, None) + if not fmt: + fmt = av.lib.av_guess_format(defaultCodec, None ,None) + + if not fmt: + raise RuntimeError('Could not find a valid output format') + + # Allocate the output media context. + self.pFormatCtx = av.lib.avformat_alloc_context() + if not self.pFormatCtx: + raise IOError(avError(res)) + + self.pFormatCtx.contents.oformat = fmt + self.pFormatCtx.contents.filename = _mediaName + + self.pkt = None + self.videoOutBuffer = None + self.mode = mode + + def __del__(self): + + if self.pFormatCtx: + + for i in range(self.pFormatCtx.contents.nb_streams): + cStream = self.pFormatCtx.contents.streams[i] + av.lib.avcodec_close(cStream.contents.codec) + + if self.mode == 'r': + + av.lib.avformat_close_input(self.pFormatCtx) + + elif self.mode == 'w': + + if self.videoOutBuffer: + av.lib.av_free(self.videoOutBuffer) + + pAvioCtx = self.pFormatCtx.contents.pb + + if pAvioCtx: + res = av.lib.avio_close(pAvioCtx) + if res < 0: + raise IOError(avError(res)) + + av.lib.avformat_free_context(self.pFormatCtx) + + def info(self): + + ''' Get media information + + :return: dict with the following fields: name, metadata, stream, duration + :rtype: dict + + * duration: media duration in seconds + * name: media filename + * stream: list of stream info (dict) + + .. note:: + each stream info will contains the following fields: + + - all: + - codec: codec short name + - type: video, audio or subtitle + - video: + - widht, height: video size + - fps: as a tuple of 3 values (num, den, ticks) + - pixelFormat: pixel format name (ie. rgb24, yuv420p ...) + - audio: + - sampleRate: sample rate + - channels: channel count + - sampleFmt: sample format name (ie. s16 ...) + - sampleFmtId: sample format id (internal use) + - frameSize: frame size + - bytesPerSample: bytes for sample format (ie. 2 for s16) + - subtitle: + - subtitle header: header string + + .. seealso:: :meth:`writeHeader` + ''' + + infoDict = {} + infoDict['name'] = toString(self.pFormatCtx.contents.filename) + + if self.mode == 'r': + avFormat = self.pFormatCtx.contents.iformat + else: + avFormat = self.pFormatCtx.contents.oformat + + infoDict['format'] = toString(avFormat.contents.name) + infoDict['metadata'] = self.metadata() + infoDict['stream'] = [] + infoDict['duration'] = float(self.pFormatCtx.contents.duration)/av.lib.AV_TIME_BASE + + for i in range(self.pFormatCtx.contents.nb_streams): + cStream = self.pFormatCtx.contents.streams[i] + cStreamInfo = self._streamInfo(cStream) + if cStreamInfo: + infoDict['stream'].append(cStreamInfo) + + return infoDict + + def _streamInfo(self, stream): + + streamInfo = {} + cCodecCtx = stream.contents.codec + + # cCodecCtx.contents.codec is NULL so retrieve codec using id + # avcodec_find_decoder can return a null value: AV_CODEC_ID_FIRST_UNKNOWN + c = av.lib.avcodec_find_decoder(cCodecCtx.contents.codec_id) + + if c: + streamInfo['codec'] = toString(c.contents.name) + + codecType = cCodecCtx.contents.codec_type + if codecType == av.lib.AVMEDIA_TYPE_VIDEO: + streamInfo['type'] = 'video' + streamInfo['width'] = cCodecCtx.contents.width + streamInfo['height'] = cCodecCtx.contents.height + streamInfo['fps'] = (cCodecCtx.contents.time_base.num, + cCodecCtx.contents.time_base.den, + cCodecCtx.contents.ticks_per_frame) + + try: + pixFmtName = av.lib.avcodec_get_pix_fmt_name + except AttributeError: + pixFmtName = av.lib.av_get_pix_fmt_name + + streamInfo['pixelFormat'] = toString(pixFmtName(cCodecCtx.contents.pix_fmt)) + elif codecType == av.lib.AVMEDIA_TYPE_AUDIO: + + streamInfo['type'] = 'audio' + streamInfo['sampleRate'] = cCodecCtx.contents.sample_rate + streamInfo['channels'] = cCodecCtx.contents.channels + streamInfo['sampleFmt'] = toString(av.lib.av_get_sample_fmt_name(cCodecCtx.contents.sample_fmt)) + streamInfo['sampleFmtId'] = cCodecCtx.contents.sample_fmt + streamInfo['frameSize'] = cCodecCtx.contents.frame_size + + if streamInfo['frameSize'] == 0: + streamInfo['frameSize'] = FRAME_SIZE_DEFAULT + + streamInfo['channels'] = cCodecCtx.contents.channels + streamInfo['bytesPerSample'] = av.lib.av_get_bytes_per_sample(cCodecCtx.contents.sample_fmt) + bufSize = 128 + buf = ctypes.create_string_buffer(bufSize) + av.lib.av_get_channel_layout_string( + buf, bufSize, + streamInfo['channels'], cCodecCtx.contents.channel_layout) + + streamInfo['channelLayoutDescription'] = buf.value + streamInfo['channelLayoutId'] = cCodecCtx.contents.channel_layout + + if hasattr(av.lib, 'av_get_channel_name'): + # av_get_channel_name return None if format is planar + streamInfo['channelLayout'] = av.lib.av_get_channel_name( + cCodecCtx.contents.channel_layout) or '' + else: + # libav8 only + # dirty workaround so user can easily guess if audio resampling is + # is needed (by comparing channel layout strings) + # see outputWav example + # a proper fix could be to add a function like needResampling(...) + streamInfo['channelLayout'] = streamInfo['channelLayoutDescription'] + + elif codecType == av.lib.AVMEDIA_TYPE_SUBTITLE: + + streamInfo['type'] = 'subtitle' + streamInfo['subtitleHeader'] = toString(ctypes.string_at(cCodecCtx.contents.subtitle_header, + cCodecCtx.contents.subtitle_header_size)) + else: + pass + + return streamInfo + + def metadata(self): + + ''' Get media metadata + + :return: a dict with key, value = metadata key, metadata value + + .. note:: method is also called by :meth:`info` + + .. seealso:: :meth:`writeHeader` + ''' + + done = False + metaDict = {} + tag = ctypes.POINTER(av.lib.AVDictionaryEntry)() + + while not done: + tag = av.lib.av_dict_get(self.pFormatCtx.contents.metadata, ''.encode('ascii'), tag, av.lib.AV_DICT_IGNORE_SUFFIX) + if tag: + metaDict[toString(tag.contents.key)] = toString(tag.contents.value) + else: + done = True + + return metaDict + + + # read + def __iter__(self): + return self + + def __next__(self): + # python3 require __next__, python2 next + return self.next() + + def next(self): + + ''' Iterate over Media + + :rtype: :class:`Packet` + ''' + + if self.pkt is None: + self.pkt = Packet(self.pFormatCtx) + + while av.lib.av_read_frame(self.pFormatCtx, self.pkt.pktRef) >= 0: + return self.pkt + + # end of generator + raise StopIteration + + def addScaler(self, streamIndex, width, height, + pixelFormat='rgb24', + scaling='bilinear' + ): + + ''' Add a scaler + + A scaler is responsible for: + + - scaling a video + - converting output data format (ie yuv420p to rgb24) + + :param streamIndex: stream index + :type streamIndex: int + :param with: new stream width + :type width: int + :param height: new stream height + :type height: int + :param pixelFormat: output pixel format + :type pixelFormat: str + :param scaling: scaling algorithm + :type scaling: str + + .. seealso:: + * https://en.wikipedia.org/wiki/Image_scaling + + .. note:: + Available scaling algorithm + + - fast_bilinear + - bilinear + - bicubic + - area + - bicubiclin (bicubic for luma, bilinear for chroma) + - gauss + - sinc + - lanczos + - spline + + ''' + + if self.pkt is None: + self.pkt = Packet(self.pFormatCtx) + + if pixelFormat == 'rgb24': + pixelFormatId = av.lib.PIX_FMT_RGB24 + else: + pixelFormatId = av.lib.av_get_pix_fmt(pixelFormat) + + if scaling == 'bilinear': + scalingId = av.lib.SWS_BILINEAR + else: + scalingId = _guessScaling(scaling) + if not scalingId: + raise ValueError('Unknown scaling algorithm %s' % scaling) + + scaler = (streamIndex, width, height, pixelFormatId, scalingId) + + self.pkt.scaler[streamIndex] = scaler + self.pkt._addScaler(streamIndex) + + def addResampler(self, streamIndex, inAudio, outAudio): + + ''' Add an audio resampler + + A resampler is responsible for: + + - resampling audio (frequency, layout) + - converting output data format (ie s16p -> s16) + + :param inAudio: audio input info + :type inAudio: dict + :param outAudio: audio output info + :type outAudio: dict + + .. note:: + each audio info requires the following fields: + + - sampleRate: sample rate + - sampleFmt: sample format name (ie s16) + - layout (optional): channel layout (ie. mono, stereo...) + - channels (optional): channel count + + if 'layout' is not specified, its value is guessed from the channel + count + + ''' + + if self.pkt is None: + self.pkt = Packet(self.pFormatCtx) + + resampler = (inAudio, outAudio) + self.pkt.resampler[streamIndex] = resampler + return self.pkt.addResampler(streamIndex) + + def addStream(self, streamType, streamInfo): + + ''' Add a stream + + After opening a media for encoding (writing), a stream of + desired type (video or audio) have to be added. + + :param streamType: video or audio stream + :type streamType: str + :param streamInfo: stream parameters + :type streamInfo: dict + + Supported stream parameters: + + - video: + - width, height: video size + - pixelFormat: pixel format name (ie. rgb24, yuv420p ...) + - codec: video codec name (ie. mp4), auto is a valid value + - timeBase: as a tuple (ie (1, 25) for 25 fps) + - bitRate: average bit rate (ie 64000 for 64kbits/s) + - audio: + - sampleRate: sample frequency (ie. 44100 for 44.1 kHz) + - bitRate: average bit rate (see video bit rate) + - channels channel count (ie. usually 2) + - codec: audio codec name, auto is a valid value + - sampleFmt: sample format (ie flt for float) + + More parameters are documented here: :doc:`../api/encoding` + + .. note:: + + - For an image, timeBase and bitRate parameters are ignored + - More parameters will be supported in the near futur + - Subtitle streams are not yet supported + + .. note:: use :func:`codecInfo` to query codec caps + + ''' + + if streamType == 'video': + + _codecRequested = streamInfo.get('codec', 'auto') + + if sys.version_info >= (3, 0): + codecRequested = ctypes.c_char_p(_codecRequested.encode('utf-8')) + else: + codecRequested = _codecRequested + + if _codecRequested == 'auto': + oformat = self.pFormatCtx.contents.oformat + codecId = av.lib.av_guess_codec(oformat, + None, self.pFormatCtx.contents.filename, + None, av.lib.AVMEDIA_TYPE_VIDEO) + _codec = av.lib.avcodec_find_encoder(codecId) + + else: + _codec = av.lib.avcodec_find_encoder_by_name(codecRequested) + if _codec: + codecId = _codec.contents.id + + # find the audio encoder + if not _codec: + + if 'codec' not in streamInfo or streamInfo['codec'] == 'auto': + exceptMsg = 'Codec for format %s not found' % self.pFormatCtx.contents.oformat.contents.name + else: + exceptMsg = 'Codec %s not found' % codecRequested + + raise RuntimeError(exceptMsg) + + # XXX: use COMPLIANCE_NORMAL or COMPLIANCE_STRICT? + res = av.lib.avformat_query_codec(self.pFormatCtx.contents.oformat, codecId, av.lib.FF_COMPLIANCE_STRICT) + + # Note: + # 1 -> codec supported by output format + # 0 -> unsupported + # < 0 -> not available + # for safety reason, raise for <= 0 + # ex: codec mp2 in ogg report < 0 but can crash encoder + errorTpl = (_codec.contents.name, self.pFormatCtx.contents.oformat.contents.name) + if res == 0: + raise RuntimeError('Codec %s not supported in %s muxer' % errorTpl ) + elif res < 0: + print('Warning: could not determine if codec %s is supported by %s muxer' % errorTpl) + + stream = av.lib.avformat_new_stream(self.pFormatCtx, _codec) + if not stream: + raise RuntimeError('Could not alloc stream') + + c = stream.contents.codec + + av.lib.avcodec_get_context_defaults3(c, _codec) + + if 'bitRate' in streamInfo: + c.contents.bit_rate = streamInfo['bitRate'] + + c.contents.width = streamInfo['width'] + c.contents.height = streamInfo['height'] + + # starting from libav 11 - time base has to be set on + # stream instead of codec + # but stream.contents.time_base exists for all version + # so we have no choice but to check lib version + if av.lib._libraries['name'] == 'libav' and \ + av.lib._libraries['version'] >= 11: + timeBase = stream.contents.time_base + else: + timeBase = c.contents.time_base + + if 'timeBase' in streamInfo: + timeBase.den = streamInfo['timeBase'][1] + timeBase.num = streamInfo['timeBase'][0] + else: + # note: even for writing an image, a time base is required + timeBase.den = FPS_DEFAULT[1] + timeBase.num = FPS_DEFAULT[0] + + # more parameters + if 'gopSize' in streamInfo: + c.contents.gop_size = streamInfo['gopSize'] + + if 'maxBFrames' in streamInfo: + c.contents.maxi_b_frames = sreamInfo['maxBFrames'] + + if 'mbDecision' in streamInfo: + c.contents.mb_decision = streamInfo['mbDecision'] + + if sys.version_info >= (3, 0): + pixFmt = ctypes.c_char_p(streamInfo['pixelFormat'].encode('utf8')) + else: + pixFmt = streamInfo['pixelFormat'] + + c.contents.pix_fmt = av.lib.av_get_pix_fmt(pixFmt) + + if self.pFormatCtx.contents.oformat.contents.flags & av.lib.AVFMT_GLOBALHEADER: + c.contents.flags |= av.lib.CODEC_FLAG_GLOBAL_HEADER + + # open codec + res = av.lib.avcodec_open2(c, _codec, None) + if res < 0: + raise RuntimeError(avError(res)) + + # FIXME: only alloc for libav8? + self.videoOutBufferSize = av.lib.avpicture_get_size(c.contents.pix_fmt, + streamInfo['width'], streamInfo['height']) + self.videoOutBuffer = ctypes.cast(av.lib.av_malloc(self.videoOutBufferSize), + ctypes.POINTER(ctypes.c_ubyte)) + self.outStream = stream + + elif streamType == 'audio': + + _codecRequested = streamInfo.get('codec', 'auto') + + if sys.version_info >= (3, 0): + codecRequested = ctypes.c_char_p(_codecRequested.encode('utf-8')) + else: + codecRequested = _codecRequested + + if _codecRequested == 'auto': + + oformat = self.pFormatCtx.contents.oformat + codecId = av.lib.av_guess_codec(oformat, + None, self.pFormatCtx.contents.filename, + None, av.lib.AVMEDIA_TYPE_AUDIO) + _codec = av.lib.avcodec_find_encoder(codecId) + + else: + _codec = av.lib.avcodec_find_encoder_by_name(codecRequested) + if _codec: + codecId = _codec.contents.id + + # find the audio encoder + if not _codec: + + if 'codec' not in streamInfo or streamInfo['codec'] == 'auto': + exceptMsg = 'Codec for format %s not found' % self.pFormatCtx.contents.oformat.contents.name + else: + exceptMsg = 'Codec %s not found' % _codec.contents.name + + raise RuntimeError(exceptMsg) + + # XXX: use COMPLIANCE_NORMAL or COMPLIANCE_STRICT? + res = av.lib.avformat_query_codec(self.pFormatCtx.contents.oformat, codecId, av.lib.FF_COMPLIANCE_STRICT) + + # Note: + # 1 -> codec supported by output format + # 0 -> unsupported + # < 0 -> not available + # for safety reason, raise for <= 0 + # ex: codec mp2 in ogg report < 0 but can crash encoder + errorTpl = (_codec.contents.name, self.pFormatCtx.contents.oformat.contents.name) + if res == 0: + raise RuntimeError('Codec %s not supported in %s muxer' % errorTpl ) + elif res < 0: + print('Warning: could not determine if codec %s is supported by %s muxer' % errorTpl) + + stream = av.lib.avformat_new_stream(self.pFormatCtx, _codec) + if not stream: + raise RuntimeError('Could not alloc stream') + + c = stream.contents.codec + + av.lib.avcodec_get_context_defaults3(c, _codec) + + # sample parameters + if 'sampleFmt' in streamInfo: + + _sfmt = streamInfo.get('sampleFmt', '') + if sys.version_info >= (3, 0): + _sfmt = _sfmt.encode('utf-8') + + sampleFmt = av.lib.av_get_sample_fmt(_sfmt) + else: + # default to signed 16 bits sample format + sampleFmt = av.lib.AV_SAMPLE_FMT_S16 + + # check if sample format is supported by codec + # TODO: facto. codec - see codecInfo + sfmtOk = False + for sfmt in _codec.contents.sample_fmts: + if sfmt == av.lib.AV_SAMPLE_FMT_NONE: + break + else: + if sampleFmt == sfmt: + sfmtOk = True + break + + if not sfmtOk: + raise RuntimeError('Sample format %s not supported' % av.lib.av_get_sample_fmt_name(sampleFmt)) + + c.contents.sample_fmt = sampleFmt + c.contents.bit_rate = streamInfo['bitRate'] + c.contents.sample_rate = streamInfo['sampleRate'] + + nbChannels = streamInfo['channels'] + c.contents.channels = nbChannels + + if hasattr(av.lib, 'av_get_default_channel_layout'): + f = av.lib.av_get_default_channel_layout + else: + f = _guessChannelLayout + + c.contents.channel_layout = f(nbChannels) + + if self.pFormatCtx.contents.oformat.contents.flags & av.lib.AVFMT_GLOBALHEADER: + c.contents.flags |= av.lib.CODEC_FLAG_GLOBAL_HEADER + + # open codec + res = av.lib.avcodec_open2(c, _codec, None) + if res < 0: + raise RuntimeError(avError(res)) + + # FIXME: multiple stream + self.outStream = stream + + else: + raise RuntimeError('Unknown stream type (not video or audio)') + + def writeHeader(self, metaData=None): + + ''' Write media header + + Write media header. This method have to be called before any + call to :meth:`write` + + :param metaData: media metaData (ie. artist, year ...) + :type metaData: dict + + .. note:: + see http://multimedia.cx/eggs/supplying-ffmpeg-with-metadata/ + for available metadata per container + ''' + + fmt = self.pFormatCtx.contents.oformat + fn = self.pFormatCtx.contents.filename + + res = av.lib.avio_open(ctypes.byref(self.pFormatCtx.contents.pb), + fn, av.lib.AVIO_FLAG_WRITE) + if res < 0: + raise IOError(avError(res)) + + if metaData: + for k in metaData: + + v = metaData[k] + if sys.version_info >= (3, 0): + k = ctypes.c_char_p(k.encode('utf-8')) + v = ctypes.c_char_p(v.encode('utf-8')) + + metaDict = ctypes.POINTER(ctypes.POINTER(av.lib.AVDictionary))() + metaDict.contents = self.pFormatCtx.contents.metadata + + av.lib.av_dict_set(metaDict, + k, v, + av.lib.AV_DICT_IGNORE_SUFFIX) + + # write the stream header, if any + # libav8 -> av_write_header, libav9 -> avformat... + if hasattr(av.lib, 'av_write_header'): + av.lib.av_write_header(self.pFormatCtx) + else: + av.lib.avformat_write_header(self.pFormatCtx, None) + + def writeTrailer(self): + + ''' Write media trailer + + Write media trailer. Call this method just before closing + or deleting media. + ''' + + av.lib.av_write_trailer(self.pFormatCtx) + + def videoPacket(self): + + ''' Get a video packet ready for encoding purpose + + Initialize and allocate data for a video packet. Data format will + depend to the previously added stream. + + :return: video packet + :rtype: :class:`Packet` + ''' + + self.pkt = Packet(self.pFormatCtx) + + c = self.outStream.contents.codec + width = c.contents.width + height = c.contents.height + pixFmt = c.contents.pix_fmt + + av.lib.avpicture_alloc( + ctypes.cast(self.pkt.frame, ctypes.POINTER(av.lib.AVPicture)), + pixFmt, + width, + height) + + return self.pkt + + def audioPacket(self): + + ''' Get an audio packet ready for encoding purpose + + Initialize and allocate data for an audio packet. Data format will + depend to the previously added stream. + + :return: video packet + :rtype: :class:`Packet` + + :raises: RuntimeError if data could not be allocated + ''' + + c = self.outStream.contents.codec + + if c.contents.frame_size == 0: + # frame size is set to 0 for pcm codec + bufSize = FRAME_SIZE_DEFAULT + else: + bufSize = av.lib.av_samples_get_buffer_size(None, c.contents.channels, c.contents.frame_size, + c.contents.sample_fmt, 0) + + buf = av.lib.av_malloc(bufSize) + + self.pkt = Packet(self.pFormatCtx) + + av.lib.avcodec_get_frame_defaults(self.pkt.frame) + + self.pkt.frame.contents.nb_samples = int(bufSize / + (c.contents.channels * av.lib.av_get_bytes_per_sample(c.contents.sample_fmt))) + + res = av.lib.avcodec_fill_audio_frame(self.pkt.frame, + c.contents.channels, c.contents.sample_fmt, + ctypes.cast(buf, ctypes.POINTER(ctypes.c_ubyte)), bufSize, 0) + + if res < 0: + raise RuntimeError('fill audio frame failed') + + return self.pkt + + def write(self, packet, pts, mediaType='video'): + + ''' Write packet to media + + :param packet: packet to encode and add to media + :type packet: :class:`Packet` or array.array or numpy.array + ''' + + c = self.outStream.contents.codec + st = self.outStream + + if mediaType == 'video': + + # guess if we should use packet.frame or packet.swsFrame + + if isinstance(packet, array.array): + + # pktFrame = a.buffer_info()[0] + bufAddr, bufLen = packet.buffer_info() + + # create a frame + buf = ctypes.cast(bufAddr, ctypes.POINTER(ctypes.c_ubyte)) + pktFrame = av.lib.avcodec_alloc_frame() + pktFrame.contents.data[0] = buf + + pktFrame.contents.width = c.contents.width + pktFrame.contents.height = c.contents.height + pktFrame.contents.linesize[0] = av.lib.avpicture_get_size(c.contents.pix_fmt, + c.contents.width, 1) + + elif isinstance(packet, Packet): + pktFrame = packet.frame + swsCtx = packet.swsCtx[packet.streamIndex()] + if swsCtx: + pktFrame = packet.swsFrame + else: + + pktFrame = None + + try: + import numpy + except ImportError: + pass + else: + buf = packet.ctypes.data_as(ctypes.POINTER(ctypes.c_ubyte)) + pktFrame = av.lib.avcodec_alloc_frame() + pktFrame.contents.data[0] = buf + + pktFrame.contents.width = c.contents.width + pktFrame.contents.height = c.contents.height + pktFrame.contents.linesize[0] = av.lib.avpicture_get_size(c.contents.pix_fmt, + c.contents.width, 1) + + if not pktFrame: + raise RuntimeError + + # libav8: encode_video, libav 9 or > encode_video2 + if hasattr(av.lib, 'avcodec_encode_video'): + + encSize = av.lib.avcodec_encode_video(c, + self.videoOutBuffer, self.videoOutBufferSize, pktFrame) + + if encSize > 0: + + pkt = av.lib.AVPacket() + pktRef = ctypes.byref(pkt) + + av.lib.av_init_packet(pktRef) + + if c.contents.coded_frame.contents.pts != av.lib.AV_NOPTS_VALUE: + + pkt.pts = pts + + if c.contents.coded_frame.contents.key_frame: + pkt.flags |= av.lib.AV_PKT_FLAG_KEY + + pkt.stream_index = st.contents.index + + pkt.data = self.videoOutBuffer + pkt.size = encSize + + ret = av.lib.av_interleaved_write_frame(self.pFormatCtx, pktRef) + else: + + outPkt = av.lib.AVPacket() + outPktRef = ctypes.byref(outPkt) + outPkt.data = None + + self.decoded = ctypes.c_int(-1) + self.decodedRef = ctypes.byref(self.decoded) + + av.lib.av_init_packet(outPktRef) + + encSize = av.lib.avcodec_encode_video2(c, outPktRef, pktFrame, self.decodedRef) + + if outPkt.size > 0: + pktRef = ctypes.byref(outPkt) + ret = av.lib.av_interleaved_write_frame(self.pFormatCtx, pktRef) + + elif mediaType == 'audio': + + outPkt = av.lib.AVPacket() + outPktRef = ctypes.byref(outPkt) + outPkt.data = None + + self.decoded = ctypes.c_int(-1) + self.decodedRef = ctypes.byref(self.decoded) + + av.lib.av_init_packet(outPktRef) + + encSize = av.lib.avcodec_encode_audio2(c, outPktRef, packet.frame, self.decodedRef) + + if outPkt.size > 0: + pktRef = ctypes.byref(outPkt) + ret = av.lib.av_interleaved_write_frame(self.pFormatCtx, pktRef) + + else: + raise RuntimeError('Unknown media type %s' % mediaType) + + @staticmethod + @contextlib.contextmanager + def open(mediaName, mode=None, buffering=None, **kwargs): + + ''' Open a media file (writing only) + + :param mediaName: media name + :type mediaName: str + :param mode: open media mode (ignored) + :type mode: str + :param buffering: buffering (ignored) + :type buffering: int + :param metaData: meta data dict (optional) + :type metaData: dict + :param streamsInfo: stream(s) info + :type streamsInfo: dict + + ''' + + m = Media(mediaName, mode) + + metadata = kwargs.get('metaData') + streamsInfo = kwargs.get('streamsInfo') + + try: + + for streamInfo in streamsInfo: + m.addStream(streamInfo['type'], streamInfo) + m.writeHeader(metadata) + yield m + except Exception: + # import traceback + # print traceback.format_exc() + raise + else: + m.writeTrailer() + + +class Packet(object): + + ''' Media data container + + When decoding a media, a packet object will be returned and + might be decoded later. + + When encoding, a packet is retrieved then written. + ''' + + def __init__(self, formatCtx): + + # alloc packet and keep a ref (for Media.__next__) + self.pkt = av.lib.AVPacket() + self.pktRef = ctypes.byref(self.pkt) + # alloc frame + self.frame = av.lib.avcodec_alloc_frame() + self.decoded = ctypes.c_int(-1) + self.decodedRef = ctypes.byref(self.decoded) + # subtitle + self.subtitle = av.lib.AVSubtitle() + + # frame after conversion by scaler + self.swsFrame = None + # frame after conversion by audio resampler + self.resampledFrame = None + + # after decode + self.dataSize = -1 + + # retrieve a list of codec context for each stream in file + self.codecCtx = [] + self.swsCtx = [] + self.scaler = [] + self.resampler = [] + self.resamplerCtx = [] + + self.f = formatCtx + formatCtxCt = formatCtx.contents + streamCount = formatCtxCt.nb_streams + for i in range(streamCount): + + cStream = formatCtxCt.streams[i] + cCodecCtx = cStream.contents.codec + cCodec = av.lib.avcodec_find_decoder(cCodecCtx.contents.codec_id) + + if not cCodec: + self.codecCtx.append(None) + else: + av.lib.avcodec_open2(cCodecCtx, cCodec, None) + self.codecCtx.append(cCodecCtx) + + self.swsCtx.append(None) + self.scaler.append(None) + self.resampler.append(None) + self.resamplerCtx.append(None) + + def _addScaler(self, streamIndex): + + ''' Add a scaler + + .. note:: called by :meth:`Media.addScaler` + ''' + + scaler = self.scaler[streamIndex] + + width = scaler[1] + height = scaler[2] + pixelFmt = scaler[3] + scalingId = scaler[4] + codecCtx = self.codecCtx[streamIndex] + + print(scalingId, av.lib.SWS_BILINEAR) + #raise + + self.swsCtx[streamIndex] = av.lib.sws_getContext( + codecCtx.contents.width, + codecCtx.contents.height, + codecCtx.contents.pix_fmt, + width, + height, + pixelFmt, + #av.lib.SWS_BILINEAR, + scalingId, + None, + None, + None) + + self.swsFrame = av.lib.avcodec_alloc_frame() + if not self.swsFrame: + raise RuntimeError('Could not alloc frame') + + # FIXME perf - let the user alloc a buffer? + av.lib.avpicture_alloc( + ctypes.cast(self.swsFrame, ctypes.POINTER(av.lib.AVPicture)), + pixelFmt, + width, + height) + + def __copy__(self): + + p = Packet(self.f) + + #ctypes.memmove(p.pktRef, self.pktRef, ctypes.sizeof(av.lib.AVPacket)) + + pkt = p.pkt + srcPkt = self.pkt + + if hasattr(av.lib, 'av_packet_move_ref'): + av.lib.av_packet_move_ref(pkt, srcPkt) + else: + pkt.pts = srcPkt.pts + pkt.dts = srcPkt.dts + pkt.size = srcPkt.size + pkt.stream_index = srcPkt.stream_index + pkt.flags = srcPkt.flags + pkt.side_data_elems = srcPkt.side_data_elems + pkt.duration = srcPkt.duration + pkt.destruct = srcPkt.destruct + pkt.pos = srcPkt.pos + pkt.convergence_duration = srcPkt.convergence_duration + + # data copy + data_size = pkt.size * ctypes.sizeof(av.lib.uint8_t) + pkt.data = ctypes.cast( av.lib.av_malloc(data_size), ctypes.POINTER(av.lib.uint8_t)) + # XXX: use memcpy from libavcodec? + ctypes.memmove(pkt.data, srcPkt.data, data_size) + + # side data copy + side_data_size = pkt.side_data_elems * ctypes.sizeof(av.lib.N8AVPacket4DOT_30E) + p.side_data = ctypes.cast(av.lib.av_malloc(side_data_size), + ctypes.POINTER(av.lib.N8AVPacket4DOT_30E)) + + # XXX: use memcpy from libavcodec? + ctypes.memmove(pkt.side_data, srcPkt.side_data, side_data_size) + + # scaler copy + for streamIndex, scaler in enumerate(self.scaler): + if scaler: + p.scaler[streamIndex] = scaler + p._addScaler(streamIndex) + + return p + + def __del__(self): + + av.lib.avsubtitle_free(ctypes.byref(self.subtitle)) + + for ctx in self.swsCtx: + av.lib.sws_freeContext(ctx) + + if self.swsFrame: + av.lib.avpicture_free(ctypes.cast(self.swsFrame, ctypes.POINTER(av.lib.AVPicture))) + + for ctx in self.resamplerCtx: + if ctx: + if AVPY_LIB_NAME == 'ffmpeg': + av.lib.swr_free(ctx) + else: + av.lib.avresample_close(ctx) + av.lib.avresample_free(ctx) + + if self.resampledFrame and self.resampledFrame != self.frame: + av.lib.av_free(self.resampledFrame) + + av.lib.av_free(self.frame) + av.lib.av_free_packet(ctypes.byref(self.pkt)) + + def addResampler(self, streamIndex): + + ''' Add an audio resampler + + .. note:: called by :meth:`Media.addResampler` + ''' + + def queryAudioInfos(infos): + + sampleFmt = toCString(infos['sampleFmt']) + infos['sampleFmtId'] = av.lib.av_get_sample_fmt(sampleFmt) + + if 'channelLayout' not in infos or not infos['channelLayout']: + + # guess channel layout from number of channels + + if hasattr(av.lib, 'av_get_default_channel_layout'): + + f = av.lib.av_get_default_channel_layout + else: + f = _guessChannelLayout + + infos['layoutId'] = f(infos['channels']) + + else: + + infos['layoutId'] = av.lib.av_get_channel_layout(infos['channelLayout']) + + if 'channels' not in infos: + infos['channels'] = av.lib.av_get_channel_layout_nb_channels(infos['layoutId']) + # in + inAudio = self.resampler[streamIndex][0] + queryAudioInfos(inAudio) + + # out + outAudio = self.resampler[streamIndex][1] + queryAudioInfos(outAudio) + + result = False + if inAudio != outAudio: + + # try to setup decoder (faster) else use audio resampling + if not self._addCodecResampler(streamIndex, inAudio, outAudio): + + if 'libswresample.so' in av.lib._libraries: + self._addSwResampler(streamIndex, inAudio, outAudio) + elif 'libavresample.so' in av.lib._libraries: + self._addAvResampler(streamIndex, inAudio, outAudio) + else: + raise RuntimeError('No resampling lib available') + + result = True + + return result + + def _addCodecResampler(self, streamIndex, inAudio, outAudio): + + codecCtx = self.codecCtx[streamIndex] + codec = av.lib.avcodec_find_decoder(codecCtx.contents.codec_id) + newCodecCtx = av.lib.avcodec_alloc_context3(codec) + res = av.lib.avcodec_copy_context(newCodecCtx, codecCtx) + + newCodecCtx.contents.request_sample_fmt = outAudio['sampleFmtId'] + newCodecCtx.contents.request_sample_rate = outAudio['sampleRate'] + newCodecCtx.contents.request_channel_layout = outAudio['layoutId'] + + res = av.lib.avcodec_open2(newCodecCtx, codec, None) + + channelLayout = newCodecCtx.contents.channel_layout + # workaround libav 9 + if channelLayout == 0: + channelLayout = av.lib.av_get_default_channel_layout(newCodecCtx.contents.channels) + + if newCodecCtx.contents.sample_fmt == outAudio['sampleFmtId'] and \ + newCodecCtx.contents.sample_rate == outAudio['sampleRate'] and \ + channelLayout == outAudio['layoutId']: + + self.codecCtx[streamIndex] = newCodecCtx + return True + else: + return False + + def _addSwResampler(self, streamIndex, inAudio, outAudio): + + resampleCtx = av.lib.swr_alloc() + + av.lib.swr_alloc_set_opts(resampleCtx, + outAudio['layoutId'], outAudio['sampleFmtId'], outAudio['sampleRate'], + inAudio['layoutId'], inAudio['sampleFmtId'], inAudio['sampleRate'], + 0, None) + + result = av.lib.swr_init(resampleCtx) + + if result < 0: + raise RuntimeError(avError(result)) + + self.resamplerCtx[streamIndex] = resampleCtx + + def _addAvResampler(self, streamIndex, inAudio, outAudio): + + resampleCtx = av.lib.avresample_alloc_context() + + # in + av.lib.av_opt_set_int(resampleCtx, toCString('in_channel_layout'), + inAudio['layoutId'], 0) + av.lib.av_opt_set_int(resampleCtx, toCString('in_sample_rate'), + inAudio['sampleRate'], 0) + av.lib.av_opt_set_int(resampleCtx, toCString('in_sample_fmt'), + inAudio['sampleFmtId'], 0) + + # out + av.lib.av_opt_set_int(resampleCtx, toCString('out_channel_layout'), + outAudio['layoutId'], 0) + av.lib.av_opt_set_int(resampleCtx, toCString('out_sample_rate'), + outAudio['sampleRate'], 0) + av.lib.av_opt_set_int(resampleCtx, toCString('out_sample_fmt'), + outAudio['sampleFmtId'], 0) + + result = av.lib.avresample_open(resampleCtx) + + if result < 0: + raise RuntimeError(avError(result)) + + self.resamplerCtx[streamIndex] = resampleCtx + + def streamIndex(self): + return self.pkt.stream_index + + def _allocAudioFrame(self, resamplerInfos, outSamples): + + # alloc AVFrame + + sampleFmt = resamplerInfos[1]['sampleFmtId'] + channels = resamplerInfos[1]['channels'] + bufSize = av.lib.av_samples_get_buffer_size(None, + channels, outSamples, + sampleFmt, 0) + buf = ctypes.cast(av.lib.av_malloc(bufSize), + ctypes.POINTER(ctypes.c_ubyte)) + # silent sound - debug only + ctypes.memset(buf, 0, bufSize) + + # TODO: free frame + resampledFrame = av.lib.avcodec_alloc_frame() + + av.lib.avcodec_get_frame_defaults(resampledFrame) + + resampledFrame.contents.nb_samples = outSamples + resampledFrame.contents.format = sampleFmt + + res = av.lib.avcodec_fill_audio_frame(resampledFrame, + channels, sampleFmt, + buf, bufSize, 0) + + if res < 0: + raise RuntimeError('fill audio frame failed: %s' % avError(res)) + + return resampledFrame + + def _resampleAudio(self, resamplerCtx): + + resamplerInfos = self.resampler[self.pkt.stream_index] + + inData = self.frame.contents.extended_data + inSamples = self.frame.contents.nb_samples + + if av.lib._libraries['name'] == 'ffmpeg': + + delay = av.lib.swr_get_delay(resamplerCtx, resamplerInfos[0]['sampleRate']) + outSamples = av.lib.av_rescale_rnd( + delay+inSamples, + resamplerInfos[1]['sampleRate'], resamplerInfos[0]['sampleRate'], + av.lib.AV_ROUND_UP) + + if not self.resampledFrame or outSamples > self.resampledFrame.contents.nb_samples: + av.lib.av_free(self.resampledFrame) + self.resampledFrame = self._allocAudioFrame(resamplerInfos, outSamples) + + outSamples = av.lib.swr_convert(resamplerCtx, + self.resampledFrame.contents.extended_data, + self.resampledFrame.contents.nb_samples, + inData, + inSamples) + + self.rDataSize = av.lib.av_samples_get_buffer_size(None, + resamplerInfos[1]['channels'], + outSamples, + self.resampledFrame.contents.format, + 1) + + elif av.lib._libraries['name'] == 'libav': + + delay = av.lib.avresample_get_delay(resamplerCtx) + outSamples = av.lib.av_rescale_rnd( + delay+inSamples, + resamplerInfos[1]['sampleRate'], resamplerInfos[0]['sampleRate'], + av.lib.AV_ROUND_UP) + + if not self.resampledFrame or outSamples > self.resampledFrame.contents.nb_samples: + av.lib.av_free(self.resampledFrame) + self.resampledFrame = self._allocAudioFrame(resamplerInfos, outSamples) + + outSamples = av.lib.avresample_convert(resamplerCtx, + self.resampledFrame.contents.extended_data, + 0, + self.resampledFrame.contents.nb_samples, + inData, + 0, + inSamples) + + self.rDataSize = av.lib.av_samples_get_buffer_size(None, + resamplerInfos[1]['channels'], + outSamples, + self.resampledFrame.contents.format, + 1) + + def decode(self): + + ''' Decode data + ''' + + codecCtx = self.codecCtx[self.pkt.stream_index] + #print 'c ctx', codecCtx + if codecCtx is not None: + + codecType = codecCtx.contents.codec_type + #print 'c type', codecType, av.lib.AVMEDIA_TYPE_AUDIO + + if codecType == av.lib.AVMEDIA_TYPE_AUDIO: + result = av.lib.avcodec_decode_audio4(codecCtx, self.frame, + self.decodedRef, self.pktRef) + if result > 0 and self.decoded: + self.dataSize = av.lib.av_samples_get_buffer_size(None, + codecCtx.contents.channels, self.frame.contents.nb_samples, + codecCtx.contents.sample_fmt, 1) + + resamplerCtx = self.resamplerCtx[self.pkt.stream_index] + if resamplerCtx: + self._resampleAudio(resamplerCtx) + else: + self.resampledFrame = self.frame + self.rDataSize = self.dataSize + + else: + # FIXME: raise? + print('failed decode...') + + elif codecType == av.lib.AVMEDIA_TYPE_VIDEO: + # FIXME: avcodec_decode_video2 return result? + av.lib.avcodec_decode_video2(codecCtx, self.frame, + self.decodedRef, self.pktRef) + + if self.decoded: + + swsCtx = self.swsCtx[self.pkt.stream_index] + if swsCtx: + + srcFrame = self.frame + + av.lib.sws_scale(swsCtx, + srcFrame.contents.data, + srcFrame.contents.linesize, + 0, + codecCtx.contents.height, + self.swsFrame.contents.data, + self.swsFrame.contents.linesize) + + elif codecType == av.lib.AVMEDIA_TYPE_SUBTITLE: + av.lib.avcodec_decode_subtitle2(codecCtx, self.subtitle, + self.decodedRef, self.pktRef) + + if self.decoded: + self.subtitleCount = self.subtitle.num_rects + self.subtitleTypes = [] + + for i in xrange(self.subtitleCount): + + cType = self.subtitle.rects[i].contents.type + cTypeName = '' + # FIXME: AVSubtitleType is an enum + if cType == av.lib.SUBTITLE_BITMAP: + cTypeName = 'bitmap' + elif cType == av.lib.SUBTITLE_TEXT: + cTypeName = 'text' + elif cType == av.lib.SUBTITLE_ASS: + cTypeName = 'ass' + self.subtitleTypes.append(cTypeName) + + else: + # unsupported codec type... + pass + + +def versions(): + + ''' Return version & config & license of C libav or ffmpeg libs + + :return: dict with keys: version, configuration, license, path + :rtype: dict + ''' + + versions = {} + for lib in av.lib._libraries: + + if lib in ['name', 'version']: + continue + + prefix = lib[3:-3] + fversion = getattr(av.lib, prefix+'_version') + fconfig = getattr(av.lib, prefix+'_configuration') + flicense = getattr(av.lib, prefix+'_license') + + e = versions[lib[:-3]] = {} + _version = fversion() + # FIXME dup code in av.version + e['version'] = (_version >> 16 & 0xFF, _version >> 8 & 0xFF, _version & 0xFF) + e['configuration'] = fconfig() + e['license'] = flicense() + e['path'] = av.lib._libraries[lib]._name + + return versions + + +def avError(res): + + ''' Return an error message from an error code + + The libav or ffmpeg functions can return an error code, this function will + do its best to translate it to an human readable error message. + + :param res: error code + :type res: int + :return: error message + :rtype: str + + .. note:: if error is unknown, return 'Unknown error code %d' + ''' + + # cmdutils.c - print_error + + # setup error buffer + # TODO: size 255? + bufSize = 128 + buf = ctypes.create_string_buffer(bufSize) + errRes = av.lib.av_strerror(res, buf, bufSize) + if errRes < 0: + try: + msg = os.strerror(res) + except ValueError: + msg = 'Unknown error code %d' % res + + return msg + else: + return buf.value + + + + + +def codecs(): + + ''' Get all supported codecs + + :return: a dict with 3 keys (audio, video and subtitle). For each key, the value is a dict with 2 keys (encoding and decoding). + :rtype: dict + ''' + + codecs = { + 'audio': {'decoding': [], 'encoding': []}, + 'video': {'decoding': [], 'encoding': []}, + 'subtitle': {'decoding': [], 'encoding': []}, + } + + av.lib.av_register_all() + c = None + + while 1: + c = av.lib.av_codec_next(c) + if c: + + codecName = c.contents.name + + key1 = '' + if c.contents.type == av.lib.AVMEDIA_TYPE_VIDEO: + key1 = 'video' + elif c.contents.type == av.lib.AVMEDIA_TYPE_AUDIO: + key1 = 'audio' + elif c.contents.type == av.lib.AVMEDIA_TYPE_SUBTITLE: + key1 = 'subtitle' + + if key1: + + # libav8 has encode attribute but libav9 has encode2 + encodeAttr = None + if hasattr(c.contents, 'encode'): + encodeAttr = 'encode' + elif hasattr(c.contents, 'encode2'): + encodeAttr = 'encode2' + + if c.contents.decode: + codecs[key1]['decoding'].append(codecName) + elif getattr(c.contents, encodeAttr): + codecs[key1]['encoding'].append(codecName) + + else: + break + + return codecs + + +def formats(): + + ''' Get all supported formats + + :return: a dict with 2 keys: muxing and demuxing + :rtype: dict + ''' + + # port of show_formats function (cf cmdutils.c) + + f = {'muxing': {}, 'demuxing': {}} + + av.lib.av_register_all() + ifmt = None + ofmt = None + + while 1: + ofmt = av.lib.av_oformat_next(ofmt) + if ofmt: + k = toString(ofmt.contents.name) + f['muxing'][k] = toString(ofmt.contents.long_name) + else: + break + + while 1: + ifmt = av.lib.av_iformat_next(ifmt) + if ifmt: + k = toString(ifmt.contents.name) + f['demuxing'][k] = toString(ifmt.contents.long_name) + else: + break + + return f + + +def codecInfo(name, decode=True): + + ''' Retrieve specific codec information + + :param name: codec name + :type name: str + :param decode: codec decoder info. Set decode to False to get codec encoder info. + :type name: bool + :return: codec information + :rtype: dict + :raises: ValueError if codec name is unknown + + .. note:: + codec information keys: + + - name + - longname + - type: video, audio or subtitle + - thread: codec thread capacity + - autoThread: auto thread support + - framerates: supported frame rates + - samplerates: supported sample rates + - pixFmts: supported pixel formats + - profiles: supported encoding profiles + - sampleFmts: supported sample formats + + ''' + + # from http://new.libav.org/doxygen/master/cmdutils_8c_source.html#l00598 + # examples + # framerates -> Media.codecInfo('mpeg1video', decode=False) + # samplerates -> Media.codecInfo('mp2', decode=False) + # pix_fmt -> Media.codecInfo('ffv1', decode=False) + # profiles -> Media.codecInfo('mpeg4', decode=True) + # image -> Media.codecInfo('png', decode=True) - Media.codecInfo('gif', decode=True) + # subtitle -> Media.codecInfo('ass', decode=True) + + ci = {} + + # init lib + av.lib.av_register_all() + + if decode: + c = av.lib.avcodec_find_decoder_by_name(toCString(name)) + else: + c = av.lib.avcodec_find_encoder_by_name(toCString(name)) + + if c: + ci['name'] = toString(c.contents.name) + ci['longName'] = toString(c.contents.long_name) + + codecType = c.contents.type + if codecType == av.lib.AVMEDIA_TYPE_VIDEO: + ci['type'] = 'video' + elif codecType == av.lib.AVMEDIA_TYPE_AUDIO: + ci['type'] = 'audio' + elif codecType == av.lib.AVMEDIA_TYPE_SUBTITLE: + ci['type'] = 'subtitle' + else: + ci['type'] = 'unknown' + + # thread caps + ci['thread'] = None + ci['framerates'] = [] + ci['samplerates'] = [] + ci['pixFmts'] = [] + ci['profiles'] = [] + ci['sampleFmts'] = [] + + # thread caps + caps = c.contents.capabilities & (av.lib.CODEC_CAP_FRAME_THREADS|av.lib.CODEC_CAP_SLICE_THREADS) + if caps == (av.lib.CODEC_CAP_FRAME_THREADS|av.lib.CODEC_CAP_SLICE_THREADS): + ci['thread'] = 'frame and slice' + elif caps == av.lib.CODEC_CAP_FRAME_THREADS: + ci['thread'] = 'frame' + elif caps == av.lib.CODEC_CAP_SLICE_THREADS: + ci['thread'] = 'slice' + + # support auto thread + ci['autoThread'] = False + caps = c.contents.capabilities & (av.lib.CODEC_CAP_AUTO_THREADS) + if caps == av.lib.CODEC_CAP_AUTO_THREADS: + ci['autoThread'] = True + + # supported frame rates + fps = c.contents.supported_framerates + if fps: + for f in fps: + if not f.num and not f.den: + break + ci['framerates'].append((f.num, f.den)) + + # supported sample rates + srates = c.contents.supported_samplerates + if srates: + for r in srates: + if r==0: + break + if r.num == 0 and r.den == 0: + break + ci['samplerates'].append(r) + + # supported pixel formats + pixFmts = c.contents.pix_fmts + if pixFmts: + for p in pixFmts: + if p == av.lib.PIX_FMT_NONE: + break + + if hasattr(av.lib, 'avcodec_get_pix_fmt_name'): + f = av.lib.avcodec_get_pix_fmt_name + else: + f = av.lib.av_get_pix_fmt_name + ci['pixFmts'].append(toString(f(p))) + + # profiles + pf = c.contents.profiles + if pf: + for p in pf: + if not p.name: + break + ci['profiles'].append(toString(p.name)) + + # sample_fmts + sfmts = c.contents.sample_fmts + if sfmts: + for s in sfmts: + if s == av.lib.AV_SAMPLE_FMT_NONE: + break + ci['sampleFmts'].append(toString(av.lib.av_get_sample_fmt_name(s))) + + else: + raise ValueError('Unable to find codec %s' % name) + + return ci + diff --git a/docs/source/conf.py b/docs/source/conf.py index 326f665..5d4711c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -92,8 +92,8 @@ def __mul__(self, other): master_doc = 'index' # General information about the project. -project = u'Avpy' -copyright = u'2013-2016, sydh' +project = 'Avpy' +copyright = '2013-2016, sydh' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -245,8 +245,8 @@ def __mul__(self, other): # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'Avpy.tex', u'Avpy Documentation', - u'sydh', 'manual'), + ('index', 'Avpy.tex', 'Avpy Documentation', + 'sydh', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -275,8 +275,8 @@ def __mul__(self, other): # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'avpy', u'Avpy Documentation', - [u'sydh'], 1) + ('index', 'avpy', 'Avpy Documentation', + ['sydh'], 1) ] # If true, show URL addresses after external links. @@ -289,8 +289,8 @@ def __mul__(self, other): # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'Avpy', u'Avpy Documentation', - u'sydh', 'Avpy', 'One line description of project.', + ('index', 'Avpy', 'Avpy Documentation', + 'sydh', 'Avpy', 'One line description of project.', 'Miscellaneous'), ] @@ -310,10 +310,10 @@ def __mul__(self, other): # -- Options for Epub output ---------------------------------------------- # Bibliographic Dublin Core info. -epub_title = u'Avpy' -epub_author = u'sydh' -epub_publisher = u'sydh' -epub_copyright = u'2015, sydh' +epub_title = 'Avpy' +epub_author = 'sydh' +epub_publisher = 'sydh' +epub_copyright = '2015, sydh' # The basename for the epub file. It defaults to the project name. #epub_basename = u'Avpy' diff --git a/docs/source/conf.py.bak b/docs/source/conf.py.bak new file mode 100644 index 0000000..326f665 --- /dev/null +++ b/docs/source/conf.py.bak @@ -0,0 +1,381 @@ +# -*- coding: utf-8 -*- +# +# Avpy documentation build configuration file, created by +# sphinx-quickstart on Sun Jan 4 16:49:45 2015. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +onRtd = os.environ.get('READTHEDOCS', None) == 'True' + +if onRtd: + + # to import avpy + sys.path.insert(0, os.path.abspath('../../')) + + # mock object - designed to mock + # a subset of the python ctypes modules + class Mock(object): + + def __init__(self, *args, **kwargs): + if 'name' in kwargs: + self.name = kwargs['name'] + else: + self.name = '' + + @classmethod + def __getattr__(cls, value): + return Mock(name=value) + + def __call__(self, *args, **kwargs): + + if self.name == 'avcodec_version': + # emulate libav 11 + return 3670272 + elif self.name == 'find_library': + return '' + else: + return Mock() + + def __mul__(self, other): + return 0 + + # patch ctypes module + import ctypes + sys.modules['ctypes'] = Mock() + from avpy import av + +# warn about all references where the target cannot be found +nitpicky = True + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Avpy' +copyright = u'2013-2016, sydh' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.1.3' +# The full version, including alpha/beta/rc tags. +release = '0.1.3' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Avpydoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'Avpy.tex', u'Avpy Documentation', + u'sydh', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'avpy', u'Avpy Documentation', + [u'sydh'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'Avpy', u'Avpy Documentation', + u'sydh', 'Avpy', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False + + +# -- Options for Epub output ---------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = u'Avpy' +epub_author = u'sydh' +epub_publisher = u'sydh' +epub_copyright = u'2015, sydh' + +# The basename for the epub file. It defaults to the project name. +#epub_basename = u'Avpy' + +# The HTML theme for the epub output. Since the default themes are not optimized +# for small screen space, using the same theme for HTML and epub output is +# usually not wise. This defaults to 'epub', a theme designed to save visual +# space. +#epub_theme = 'epub' + +# The language of the text. It defaults to the language option +# or en if the language is not set. +#epub_language = '' + +# The scheme of the identifier. Typical schemes are ISBN or URL. +#epub_scheme = '' + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +#epub_identifier = '' + +# A unique identification for the text. +#epub_uid = '' + +# A tuple containing the cover image and cover page html template filenames. +#epub_cover = () + +# A sequence of (type, uri, title) tuples for the guide element of content.opf. +#epub_guide = () + +# HTML files that should be inserted before the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_pre_files = [] + +# HTML files shat should be inserted after the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_post_files = [] + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + +# The depth of the table of contents in toc.ncx. +#epub_tocdepth = 3 + +# Allow duplicate toc entries. +#epub_tocdup = True + +# Choose between 'default' and 'includehidden'. +#epub_tocscope = 'default' + +# Fix unsupported image types using the PIL. +#epub_fix_images = False + +# Scale large images. +#epub_max_image_width = 0 + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#epub_show_urls = 'inline' + +# If false, no index is generated. +#epub_use_index = True + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/examples/alsaPlay/alsaPlay.py b/examples/alsaPlay/alsaPlay.py index 63d21a8..1f921c4 100755 --- a/examples/alsaPlay/alsaPlay.py +++ b/examples/alsaPlay/alsaPlay.py @@ -43,7 +43,7 @@ if astreams: astream = astreams[0] else: - print('No audio stream in %s' % mediaInfo['name']) + print(('No audio stream in %s' % mediaInfo['name'])) sys.exit(2) # forge output audio format @@ -67,7 +67,7 @@ media.addResampler(astream, si, outAudio) except RuntimeError as e: - print('Could not add an audio resampler: %s' % e) + print(('Could not add an audio resampler: %s' % e)) sys.exit(3) else: @@ -86,7 +86,7 @@ secondSize = outAudio['channels'] * outAudio['bytesPerSample'] * outAudio['sampleRate'] decodedSize = 0 - print('playing sound of %s (%s seconds)...' % (options.media, options.length)) + print(('playing sound of %s (%s seconds)...' % (options.media, options.length))) # let's play! for i, pkt in enumerate(media): diff --git a/examples/alsaPlay/alsaPlay.py.bak b/examples/alsaPlay/alsaPlay.py.bak new file mode 100755 index 0000000..63d21a8 --- /dev/null +++ b/examples/alsaPlay/alsaPlay.py.bak @@ -0,0 +1,106 @@ +#!/usr/bin/env python + +''' +Decode audio packets in media and output them using alsa audio API + +python alsaPlay.py -m file.avi +''' + +import sys +import ctypes + +import alsaaudio + +from avpy import Media + +if __name__ == '__main__': + + # cmdline + from optparse import OptionParser + + usage = "usage: %prog -m foo.avi" + parser = OptionParser(usage=usage) + parser.add_option('-m', '--media', + help='play media') + parser.add_option('--length', + help='decode at max seconds of audio', + type='int', + default=20) + + (options, args) = parser.parse_args() + + if not options.media: + print('Please provide a media to play with -m or --media option') + sys.exit(1) + + # open media + media = Media(options.media) + # dump info + mediaInfo = media.info() + + # select first audio stream + astreams = [ i for i, s in enumerate(mediaInfo['stream']) if s['type'] == 'audio' ] + if astreams: + astream = astreams[0] + else: + print('No audio stream in %s' % mediaInfo['name']) + sys.exit(2) + + # forge output audio format + si = mediaInfo['stream'][astream] + outAudio = { + 'layout': 'stereo', + 'channels': 2, + 'sampleRate': si['sampleRate'], # keep original sample rate + 'sampleFmt': 's16', + 'bytesPerSample': 2, + } + + if outAudio['layout'] != si['channelLayout'] or\ + outAudio['channels'] != si['channels'] or\ + outAudio['sampleFmt'] != si['sampleFmt'] or\ + outAudio['sampleRate'] != si['sampleRate']: + + resampler = True + + try: + media.addResampler(astream, si, outAudio) + except RuntimeError as e: + + print('Could not add an audio resampler: %s' % e) + sys.exit(3) + + else: + resampler = False + + sampleFmt = outAudio['sampleFmt'].upper() + alsaFmt = 'PCM_FORMAT_%s_%s' % (sampleFmt, 'LE' if sys.byteorder == 'little' else 'BE') + aformat=getattr(alsaaudio, alsaFmt) + + out = alsaaudio.PCM(type=alsaaudio.PCM_PLAYBACK, mode=alsaaudio.PCM_NORMAL, card='default') + out.setchannels(outAudio['channels']) + out.setrate(outAudio['sampleRate']) + out.setformat(aformat) + + # Size in bytes required for 1 second of audio + secondSize = outAudio['channels'] * outAudio['bytesPerSample'] * outAudio['sampleRate'] + decodedSize = 0 + + print('playing sound of %s (%s seconds)...' % (options.media, options.length)) + + # let's play! + for i, pkt in enumerate(media): + + if pkt.streamIndex() == astream: + pkt.decode() + if pkt.decoded: + buf = pkt.resampledFrame.contents.data[0] + bufLen = pkt.rDataSize + out.write(ctypes.string_at(buf, bufLen)) + + decodedSize += bufLen + # stop after ~ 20s (default) + # exact time will vary depending on rDataSize + if decodedSize >= options.length*secondSize: + break + diff --git a/examples/dumpSubtitle/dumpSubtitle.py b/examples/dumpSubtitle/dumpSubtitle.py index ecfd40f..64d6dd5 100644 --- a/examples/dumpSubtitle/dumpSubtitle.py +++ b/examples/dumpSubtitle/dumpSubtitle.py @@ -37,12 +37,12 @@ if stStreams: stStream = stStreams[0] else: - print('No subtitle stream in %s' % mediaInfo['name']) + print(('No subtitle stream in %s' % mediaInfo['name'])) sys.exit(2) # dump subtitle header print('header:') - print(mediaInfo['stream'][stStream]['subtitleHeader']) + print((mediaInfo['stream'][stStream]['subtitleHeader'])) count = 0 # iterate over media and decode subtitle packet @@ -61,9 +61,9 @@ for i in range(pkt2.subtitle.num_rects): if pkt2.subtitleTypes[i] == 'text': - print(pkt2.subtitle.rects[i].contents.text) + print((pkt2.subtitle.rects[i].contents.text)) elif pkt2.subtitleTypes[i] == 'ass': - print(pkt2.subtitle.rects[i].contents.ass) + print((pkt2.subtitle.rects[i].contents.ass)) else: print('non text subtitle...') diff --git a/examples/dumpSubtitle/dumpSubtitle.py.bak b/examples/dumpSubtitle/dumpSubtitle.py.bak new file mode 100644 index 0000000..ecfd40f --- /dev/null +++ b/examples/dumpSubtitle/dumpSubtitle.py.bak @@ -0,0 +1,73 @@ +#!/usr/bin/env python + +''' +print subtitle (text based subtitles only) +''' + +import sys +import copy + +from avpy import Media + +if __name__ == '__main__': + + # cmdline + from optparse import OptionParser + + usage = "usage: %prog -m foo.avi" + parser = OptionParser(usage=usage) + parser.add_option('-m', '--media', + help='play media') + parser.add_option('-c', '--count', + type='int', + default=5, + help='limit of packet to process (default: %default)') + parser.add_option('--copyPacket', + action='store_true', + help='copy packet (debug only)') + + (options, args) = parser.parse_args() + + media = Media(options.media) + # dump info + mediaInfo = media.info() + + # select first subtitle stream + stStreams = [i for i, s in enumerate(mediaInfo['stream']) if s['type'] == 'subtitle'] + if stStreams: + stStream = stStreams[0] + else: + print('No subtitle stream in %s' % mediaInfo['name']) + sys.exit(2) + + # dump subtitle header + print('header:') + print(mediaInfo['stream'][stStream]['subtitleHeader']) + + count = 0 + # iterate over media and decode subtitle packet + for pkt in media: + if pkt.streamIndex() == stStream: + + # test only + if options.copyPacket: + pkt2 = copy.copy(pkt) + else: + pkt2 = pkt + + pkt2.decode() + if pkt2.decoded: + + for i in range(pkt2.subtitle.num_rects): + + if pkt2.subtitleTypes[i] == 'text': + print(pkt2.subtitle.rects[i].contents.text) + elif pkt2.subtitleTypes[i] == 'ass': + print(pkt2.subtitle.rects[i].contents.ass) + else: + print('non text subtitle...') + + count += 1 + if count != -1 and count > options.count: + break + diff --git a/examples/encoding/encode.py b/examples/encoding/encode.py index 7e9c7c3..e8f68f0 100644 --- a/examples/encoding/encode.py +++ b/examples/encoding/encode.py @@ -34,9 +34,9 @@ def progress(msg): # python3 only - break python2 compat #print('\r%s' % msg, end='') - print('\r%s' % msg), + print(('\r%s' % msg), end=' ') else: - print('\r%s' % msg), + print(('\r%s' % msg), end=' ') if sys.version_info >= (3, 0): xrange = range @@ -56,10 +56,10 @@ def audioFrame(self, pkt, frameSize, nbChannels): samples = ctypes.cast(pkt.frame.contents.data[0], ctypes.POINTER(ctypes.c_uint16)) - for j in xrange(frameSize/nbChannels): + for j in range(frameSize/nbChannels): samples[j] = int(math.sin(self.t)*10000) - for k in xrange(1, nbChannels): + for k in range(1, nbChannels): samples[j+k] = samples[2*j] self.t += self.tincr @@ -73,13 +73,13 @@ def fillYuvImage(picture, frameIndex, width, height): i = frameIndex # Y - for y in xrange(0, height): - for x in xrange(0, width): + for y in range(0, height): + for x in range(0, width): picture.contents.data[0][y*picture.contents.linesize[0] + x] = x + y + i*3 # Cb and Cr - for y in xrange(0, int(height/2)): - for x in xrange(0, int(width/2)): + for y in range(0, int(height/2)): + for x in range(0, int(width/2)): picture.contents.data[1][y * picture.contents.linesize[1] + x] = 128 + y + i * 2 picture.contents.data[2][y * picture.contents.linesize[2] + x] = 64 + x + i * 5 @@ -240,5 +240,5 @@ def fillYuvImage(picture, frameIndex, width, height): break media.writeTrailer() - print('done writing %s' % options.media) + print(('done writing %s' % options.media)) diff --git a/examples/encoding/encode.py.bak b/examples/encoding/encode.py.bak new file mode 100644 index 0000000..7e9c7c3 --- /dev/null +++ b/examples/encoding/encode.py.bak @@ -0,0 +1,244 @@ +#!/usr/bin/env python + +''' +Encode image, video or sound from auto generated data + +This example can only generate yuv data and s16 audio data; for other +formats the ouput will be a black image or a silent sound + +video: +* python -u examples/encoding/encode.py -m foo.avi -t video -c 15 + +image: +* python -u examples/encoding/encode.py -m foo.tiff -t image + +audio: +* python -u examples/encoding/encode.py -m foo.mp2 -t audio -c 15 + +This is a complex example, see encodeImage.py for simpler code. +''' + +import sys +import math +import ctypes + +from avpy import Media + +# Python3 compat stuff... +def progress(msg): + + ''' Print progress message + ''' + + if sys.version_info >= (3, 0): + + # python3 only - break python2 compat + #print('\r%s' % msg, end='') + print('\r%s' % msg), + else: + print('\r%s' % msg), + +if sys.version_info >= (3, 0): + xrange = range + + +class SignalGen(object): + + ''' Single tone sound generator + ''' + + def __init__(self, sampleRate): + self.t = 0 + self.tincr = 2 * math.pi * 440.0 / sampleRate + + def audioFrame(self, pkt, frameSize, nbChannels): + + samples = ctypes.cast(pkt.frame.contents.data[0], + ctypes.POINTER(ctypes.c_uint16)) + + for j in xrange(frameSize/nbChannels): + samples[j] = int(math.sin(self.t)*10000) + + for k in xrange(1, nbChannels): + samples[j+k] = samples[2*j] + + self.t += self.tincr + + +def fillYuvImage(picture, frameIndex, width, height): + + ''' Yuv image generator + ''' + + i = frameIndex + + # Y + for y in xrange(0, height): + for x in xrange(0, width): + picture.contents.data[0][y*picture.contents.linesize[0] + x] = x + y + i*3 + + # Cb and Cr + for y in xrange(0, int(height/2)): + for x in xrange(0, int(width/2)): + picture.contents.data[1][y * picture.contents.linesize[1] + x] = 128 + y + i * 2 + picture.contents.data[2][y * picture.contents.linesize[2] + x] = 64 + x + i * 5 + + +if __name__ == '__main__': + + # cmdline + from optparse import OptionParser + + usage = "usage: %prog -m foo.avi" + parser = OptionParser(usage=usage) + parser.add_option('-m', '--media', + help='play media') + parser.add_option('-t', '--mediaType', + help='media type: video, audio, both') + + parser.add_option('-c', '--frameCount', + help='number of frame to encode', + type='int', + default=125) + + # test only + parser.add_option('--forceCodec', + help='force to use codec, audio only') + parser.add_option('--forceSampleFormat', + help='force to use sample format, audio only') + parser.add_option('--forceFmt', + help='force to use pixel format, image only') + + (options, args) = parser.parse_args() + + # open + if options.media: + + # setup media encoder + media = Media(options.media, 'w', quiet=False) + + # setup stream info + if options.mediaType == 'image': + + resolution = (320, 240) + + streamInfoVideo = { + 'width': resolution[0], + 'height': resolution[1], + } + + # codec auto. guess + streamInfoVideo['codec'] = 'auto' + + if options.forceFmt: + streamInfoVideo['pixelFormat'] = options.forceFmt + else: + streamInfoVideo['pixelFormat'] = 'rgb24' + + streamIndex = media.addStream('video', streamInfoVideo) + + pkt = media.videoPacket() + + # single image -> set frameCount to 1 + options.frameCount = 1 + + elif options.mediaType == 'video': + + resolution = (320, 240) + + streamInfoVideo = { + 'bitRate': 400000, + 'width': resolution[0], + 'height': resolution[1], + # 25 fps + 'timeBase': (1, 25), + 'pixelFormat': 'yuv420p', + } + + streamIndex = media.addStream('video', streamInfoVideo) + + pkt = media.videoPacket() + + elif options.mediaType == 'audio': + + streamInfoAudio = { + 'bitRate': 64000, + 'sampleRate': 44100, + 'channels': 2, + 'codec': 'auto', + } + + if options.forceSampleFormat: + streamInfoAudio['sampleFmt'] = options.forceSampleFormat + if options.forceCodec: + streamInfoAudio['codec'] = options.forceCodec + + # No generator available if sample format is not signed 16 bit + # patch welcome! + if options.forceSampleFormat and options.forceSampleFormat != 's16': + print('Warning: will generate a silent sound!') + + streamIndex = media.addStream('audio', + streamInfo=streamInfoAudio) + + info = media.info() + frameSize = info['stream'][0]['frameSize'] + + soundGen = SignalGen(streamInfoAudio['sampleRate']) + pkt = media.audioPacket() + + elif options.mediaType == 'both': + + raise NotImplementedError() + + else: + raise RuntimeError() + + # see http://multimedia.cx/eggs/supplying-ffmpeg-with-metadata/ + # for available metadata per container + metadata = {'artist': 'me'} + media.writeHeader(metadata) + + # Presentation TimeStamp (pts) + pts = 0 + maxFrame = options.frameCount + + while True: + + if options.mediaType == 'image': + + progress('Generating image frame... ') + # TODO: need a fillRgbImage + # patch welcome! + #fillYuvImage(pkt.frame, i, *resolution) + media.write(pkt, pts+1, 'video') + + elif options.mediaType == 'video': + + progress('Generating video frame %d/%d... ' % (pts, maxFrame)) + fillYuvImage(pkt.frame, pts, *resolution) + media.write(pkt, pts+1, options.mediaType) + + elif options.mediaType == 'audio': + + progress('Generating audio frame %d/%d... ' % (pts, maxFrame)) + + if 'sampleFmt' not in streamInfoAudio or streamInfoAudio['sampleFmt'] == 's16': + soundGen.audioFrame(pkt, frameSize, streamInfoAudio['channels']) + + media.write(pkt, pts+1, options.mediaType) + + elif options.mediaType == 'both': + + raise NotImplementedError() + + else: + raise RuntimeError() + + pts+=1 + if pts >= maxFrame: + break + + media.writeTrailer() + print('done writing %s' % options.media) + diff --git a/examples/encoding/encodeImage.py b/examples/encoding/encodeImage.py index 16005d6..d21de6e 100644 --- a/examples/encoding/encodeImage.py +++ b/examples/encoding/encodeImage.py @@ -40,7 +40,7 @@ try: media = Media(options.media, quiet=False) except IOError as e: - print('Unable to open %s: %s' % (options.media, e)) + print(('Unable to open %s: %s' % (options.media, e))) sys.exit(1) # dump info @@ -51,14 +51,14 @@ if vstreams: vstream = vstreams[0] else: - print('No video stream in %s' % mediaInfo['name']) + print(('No video stream in %s' % mediaInfo['name'])) sys.exit(2) # retrieve video width and height streamInfo = mediaInfo['stream'][vstream] w, h = streamInfo['width'], streamInfo['height'] - print('video stream resolution: %dx%d' % (w, h)) + print(('video stream resolution: %dx%d' % (w, h))) # tiff format require rgb24 (24 bits) media.addScaler(vstream, w, h) @@ -85,12 +85,12 @@ if pkt2.decoded: decodedCount += 1 - print('decoded frame %d' % decodedCount) + print(('decoded frame %d' % decodedCount)) if decodedCount >= options.offset: img = 'frame.%d.tiff' % decodedCount - print('saving image %s...' % img) + print(('saving image %s...' % img)) # setup output media imgMedia = Media(img, 'w', quiet=False) diff --git a/examples/encoding/encodeImage.py.bak b/examples/encoding/encodeImage.py.bak new file mode 100644 index 0000000..16005d6 --- /dev/null +++ b/examples/encoding/encodeImage.py.bak @@ -0,0 +1,107 @@ +#!/usr/bin/env python + +''' +Decode the 5th first frame of the first video stream +and write tiff images + +Note that this example, use avpy to decode and encode image (no dependencies) + +python encodeImage.py -m file.avi -> save frame 1 to 5 +python encodeImage.py -m file.avi -o 140 -c 3 -> save frame 140 to 143 +''' + +import sys +import copy +from avpy import Media + +if __name__ == '__main__': + + # cmdline + from optparse import OptionParser + + usage = "usage: %prog -m foo.avi -o 140" + parser = OptionParser(usage=usage) + parser.add_option('-m', '--media', + help='play media') + parser.add_option('-o', '--offset', + type='int', + help='frame offset', default=0) + parser.add_option('-c', '--frameCount', + type='int', + default=5, + help='number of image to save (default: %default)') + + (options, args) = parser.parse_args() + + if not options.media: + print('Please provide a media to play with -m or --media option') + sys.exit(1) + + try: + media = Media(options.media, quiet=False) + except IOError as e: + print('Unable to open %s: %s' % (options.media, e)) + sys.exit(1) + + # dump info + mediaInfo = media.info() + + # select first video stream + vstreams = [ i for i, s in enumerate(mediaInfo['stream']) if s['type'] == 'video' ] + if vstreams: + vstream = vstreams[0] + else: + print('No video stream in %s' % mediaInfo['name']) + sys.exit(2) + + # retrieve video width and height + streamInfo = mediaInfo['stream'][vstream] + w, h = streamInfo['width'], streamInfo['height'] + + print('video stream resolution: %dx%d' % (w, h)) + + # tiff format require rgb24 (24 bits) + media.addScaler(vstream, w, h) + + # setup encoder (stream info) + streamInfoImage = { + 'width': w, + 'height': h, + 'pixelFormat': 'rgb24', + 'codec': 'tiff', + } + + decodedCount = 0 + # iterate over media + for pkt in media: + + if pkt.streamIndex() == vstream: + + # copy packet - not mandatory + # TODO: remove copy? + pkt2 = copy.copy(pkt) + pkt2.decode() + + if pkt2.decoded: + + decodedCount += 1 + print('decoded frame %d' % decodedCount) + + if decodedCount >= options.offset: + + img = 'frame.%d.tiff' % decodedCount + print('saving image %s...' % img) + + # setup output media + imgMedia = Media(img, 'w', quiet=False) + imgMedia.addStream('video', streamInfoImage) + + # write data + imgMedia.writeHeader() + imgMedia.write(pkt2, 1, 'video') + imgMedia.writeTrailer() + + #del(imgMedia) + + if decodedCount >= options.offset+options.frameCount: + break diff --git a/examples/mediaInfo/mediaInfo.py b/examples/mediaInfo/mediaInfo.py index 67a7750..e079339 100755 --- a/examples/mediaInfo/mediaInfo.py +++ b/examples/mediaInfo/mediaInfo.py @@ -12,7 +12,7 @@ from avpy import Media, formats, codecs, codecInfo def printSep(ch='='): - print(ch*25) + print((ch*25)) if __name__ == '__main__': @@ -39,23 +39,23 @@ def printSep(ch='='): infoDict = media.info() # general info - print('%s info:' % options.media) - print(' format: %s' % infoDict['format']) - print(' metadata: %s' % infoDict['metadata']) - print(' duration: %f' % infoDict['duration']) + print(('%s info:' % options.media)) + print((' format: %s' % infoDict['format'])) + print((' metadata: %s' % infoDict['metadata'])) + print((' duration: %f' % infoDict['duration'])) # stream(s) info printSep() print('stream(s):') for stream in infoDict['stream']: - print('- %s' % stream) + print(('- %s' % stream)) # codec info if options.info: - print(' %s info:' % stream['codec']) + print((' %s info:' % stream['codec'])) info = codecInfo(stream['codec']) for k in info: - print(' %s: %s' % (k, info[k])) + print((' %s: %s' % (k, info[k]))) printSep() else: print('please provide a movie or sound file') diff --git a/examples/mediaInfo/mediaInfo.py.bak b/examples/mediaInfo/mediaInfo.py.bak new file mode 100755 index 0000000..67a7750 --- /dev/null +++ b/examples/mediaInfo/mediaInfo.py.bak @@ -0,0 +1,63 @@ +#!/usr/bin/env python + +''' +Print media information such as: +- format or container (avi, mkv...) +- metadata +- duration +- stream(s) info: + - codec info +''' + +from avpy import Media, formats, codecs, codecInfo + +def printSep(ch='='): + print(ch*25) + +if __name__ == '__main__': + + # cmdline + from optparse import OptionParser + + usage = "usage: %prog -m foo.avi" + parser = OptionParser(usage=usage) + parser.add_option('-m', '--media', + help='play media') + parser.add_option('-i', '--info', + action='store_true', + help='print codec info') + (options, args) = parser.parse_args() + + formats = formats() + codecs = codecs() + + # open + if options.media: + + media = Media(options.media) + # retrieve media infos + infoDict = media.info() + + # general info + print('%s info:' % options.media) + print(' format: %s' % infoDict['format']) + print(' metadata: %s' % infoDict['metadata']) + print(' duration: %f' % infoDict['duration']) + + # stream(s) info + printSep() + print('stream(s):') + for stream in infoDict['stream']: + print('- %s' % stream) + + # codec info + if options.info: + print(' %s info:' % stream['codec']) + info = codecInfo(stream['codec']) + for k in info: + print(' %s: %s' % (k, info[k])) + printSep() + else: + print('please provide a movie or sound file') + + diff --git a/examples/misc/libInfo.py b/examples/misc/libInfo.py index c13a05c..4c60f58 100644 --- a/examples/misc/libInfo.py +++ b/examples/misc/libInfo.py @@ -26,7 +26,7 @@ help='print supported codecs (default: %default): all, video, audio, subtitle') (options, args) = parser.parse_args() - print('Using avpy version %s' % avMedia.__version__) + print(('Using avpy version %s' % avMedia.__version__)) pprint(avMedia.versions()) # supported formats (avi, mkv...) diff --git a/examples/misc/libInfo.py.bak b/examples/misc/libInfo.py.bak new file mode 100644 index 0000000..c13a05c --- /dev/null +++ b/examples/misc/libInfo.py.bak @@ -0,0 +1,44 @@ +#!/usr/bin/env python + +''' +Print information about avpy: +- Version +- C lib used +- Supported formats and codecs +''' + +from pprint import pprint + +from avpy import avMedia + +if __name__ == '__main__': + + # cmdline + from optparse import OptionParser + + usage = "usage: %prog -f" + parser = OptionParser(usage=usage) + parser.add_option('-f', '--formats', + action='store_true', + help='print supported formats') + parser.add_option('-c', '--codecs', + default='all', + help='print supported codecs (default: %default): all, video, audio, subtitle') + (options, args) = parser.parse_args() + + print('Using avpy version %s' % avMedia.__version__) + pprint(avMedia.versions()) + + # supported formats (avi, mkv...) + if options.formats: + pprint(avMedia.formats()) + + # supported codecs (h264, mp3, ass...) + if options.codecs: + codecs = avMedia.codecs() + + if options.codecs in ['audio', 'video', 'subtitle']: + pprint(codecs[options.codecs]) + else: + pprint(codecs) + diff --git a/examples/outputPgm/outputPgm.py b/examples/outputPgm/outputPgm.py index 8149b48..3e0ca7a 100755 --- a/examples/outputPgm/outputPgm.py +++ b/examples/outputPgm/outputPgm.py @@ -73,7 +73,7 @@ def savePgm(frame, w, h, index): try: media = Media(options.media) except IOError as e: - print('Unable to open %s: %s' % (options.media, e)) + print(('Unable to open %s: %s' % (options.media, e))) sys.exit(1) # dump info @@ -84,14 +84,14 @@ def savePgm(frame, w, h, index): if vstreams: vstream = vstreams[0] else: - print('No video stream in %s' % mediaInfo['name']) + print(('No video stream in %s' % mediaInfo['name'])) sys.exit(2) # retrieve video width and height streamInfo = mediaInfo['stream'][vstream] w, h = streamInfo['width'], streamInfo['height'] - print('video stream resolution: %dx%d' % (w, h)) + print(('video stream resolution: %dx%d' % (w, h))) # pgm format require rgb24 (24 bits) media.addScaler(vstream, w, h) @@ -107,7 +107,7 @@ def savePgm(frame, w, h, index): if pkt.decoded: decodedCount += 1 - print('Decoded frame %d' % decodedCount) + print(('Decoded frame %d' % decodedCount)) if decodedCount >= options.offset: print('Saving frame...') diff --git a/examples/outputPgm/outputPgm.py.bak b/examples/outputPgm/outputPgm.py.bak new file mode 100755 index 0000000..8149b48 --- /dev/null +++ b/examples/outputPgm/outputPgm.py.bak @@ -0,0 +1,118 @@ +#!/usr/bin/env python + +''' +Decode the 5th first frame of the first video stream and write pgm file + +python outputPgm.py -m file.avi -> save frame 1 to 5 +python outputPgm.py -m file.avi -o 140 -c 8 -> save frame 140 to 148 + +This is a rather low level example (no dependencies), see: +* outputPIL: use PIL (or Pillow) to write and modify image +* outputPygame: use Pygame (python2 only) to write jpg +* outputSDL2: use PySDL2 to write bmp +''' + +import sys +import itertools +import array +import ctypes +from avpy import Media + +def ptrAdd(ptr, offset): + + ''' C pointer add (see savePgm) + ''' + + address = ctypes.addressof(ptr.contents) + offset + return ctypes.pointer(type(ptr.contents).from_address(address)) + +def savePgm(frame, w, h, index): + + ''' Custom pgm writer + ''' + + #a = array.array('B', [0]*(w*3)) + a = array.array('B', itertools.repeat(0, (w*3))) + + header = 'P6\n%d %d\n255\n' % (w, h) + + if sys.version_info >= (3, 0): + header = bytes(header, 'ascii') + + with open('frame.%d.ppm' % index, 'wb') as f: + #header + f.write(header) + for i in range(h): + ptr = ptrAdd(frame.contents.data[0], i*frame.contents.linesize[0]) + ctypes.memmove(a.buffer_info()[0], ptr, w*3) + a.tofile(f) + +if __name__ == '__main__': + + # cmdline + from optparse import OptionParser + + usage = "usage: %prog -m foo.avi -o 140" + parser = OptionParser(usage=usage) + parser.add_option('-m', '--media', + help='play media') + parser.add_option('-o', '--offset', + type='int', + help='frame offset', default=0) + parser.add_option('-c', '--frameCount', + type='int', + default=5, + help='number of image to save (default: %default)') + + (options, args) = parser.parse_args() + + if not options.media: + print('Please provide a media to play with -m or --media option') + sys.exit(1) + + try: + media = Media(options.media) + except IOError as e: + print('Unable to open %s: %s' % (options.media, e)) + sys.exit(1) + + # dump info + mediaInfo = media.info() + + # select first video stream + vstreams = [ i for i, s in enumerate(mediaInfo['stream']) if s['type'] == 'video' ] + if vstreams: + vstream = vstreams[0] + else: + print('No video stream in %s' % mediaInfo['name']) + sys.exit(2) + + # retrieve video width and height + streamInfo = mediaInfo['stream'][vstream] + w, h = streamInfo['width'], streamInfo['height'] + + print('video stream resolution: %dx%d' % (w, h)) + + # pgm format require rgb24 (24 bits) + media.addScaler(vstream, w, h) + + # decode counter + decodedCount = 0 + + # iterate over media + for pkt in media: + + if pkt.streamIndex() == vstream: + pkt.decode() + if pkt.decoded: + + decodedCount += 1 + print('Decoded frame %d' % decodedCount) + + if decodedCount >= options.offset: + print('Saving frame...') + savePgm(pkt.swsFrame, w, h, decodedCount) + + if decodedCount >= options.offset+options.frameCount: + break + diff --git a/examples/outputPil/outputPil.py b/examples/outputPil/outputPil.py index 5753e45..237e4ee 100644 --- a/examples/outputPil/outputPil.py +++ b/examples/outputPil/outputPil.py @@ -48,7 +48,7 @@ try: media = Media(options.media) except IOError as e: - print('Unable to open %s: %s' % (options.media, e)) + print(('Unable to open %s: %s' % (options.media, e))) sys.exit(1) # dump info @@ -59,15 +59,15 @@ if vstreams: vstream = vstreams[0] else: - print('No video stream in %s' % mediaInfo['name']) + print(('No video stream in %s' % mediaInfo['name'])) sys.exit(2) # retrieve video width and height streamInfo = mediaInfo['stream'][vstream] size = streamInfo['width'], streamInfo['height'] - print('video stream index: %d' % vstream) - print('video stream resolution: %dx%d' % (size[0], size[1])) + print(('video stream index: %d' % vstream)) + print(('video stream resolution: %dx%d' % (size[0], size[1]))) # test only - frombuffer (default mode) is faster if options.mode == 'string': @@ -94,7 +94,7 @@ if pkt2.decoded: decodedCount += 1 - print('decoded frame %d' % decodedCount) + print(('decoded frame %d' % decodedCount)) if decodedCount >= options.offset: print('saving frame...') diff --git a/examples/outputPil/outputPil.py.bak b/examples/outputPil/outputPil.py.bak new file mode 100644 index 0000000..5753e45 --- /dev/null +++ b/examples/outputPil/outputPil.py.bak @@ -0,0 +1,126 @@ +#!/usr/bin/env python + +''' +Decode the 5th first frame of the first video stream and write grayscale png (require PIL (or pillow lib) + +python outputPil.py -m file.avi -> save frame 1 to 5 +python outputPil.py -m file.avi -o 140 -> save frame 140 to 145 +''' + +import sys +import ctypes +import copy + +from PIL import Image + +from avpy import Media + +if __name__ == '__main__': + + modeChoices = ['string', 'buffer'] + + from optparse import OptionParser + + usage = "usage: %prog -m foo.avi -o 140" + parser = OptionParser(usage=usage) + parser.add_option('-m', '--media', + help='play media') + parser.add_option('-o', '--offset', + type='int', + help='frame offset', default=0) + parser.add_option('-c', '--frameCount', + type='int', + help='number of image to save (default: %default)', default=5) + parser.add_option('--copyPacket', + action='store_true', + help='copy packet (debug only)') + parser.add_option('--mode', + type='choice', + choices=modeChoices, + default='buffer', + help='transfer from Avpy to PIL: %s' % modeChoices + '(default: %default)') + + (options, args) = parser.parse_args() + + if not options.media: + print('Please provide a media to play with -m or --media option') + sys.exit(1) + try: + media = Media(options.media) + except IOError as e: + print('Unable to open %s: %s' % (options.media, e)) + sys.exit(1) + + # dump info + mediaInfo = media.info() + + # select first video stream + vstreams = [ i for i, s in enumerate(mediaInfo['stream']) if s['type'] == 'video' ] + if vstreams: + vstream = vstreams[0] + else: + print('No video stream in %s' % mediaInfo['name']) + sys.exit(2) + + # retrieve video width and height + streamInfo = mediaInfo['stream'][vstream] + size = streamInfo['width'], streamInfo['height'] + + print('video stream index: %d' % vstream) + print('video stream resolution: %dx%d' % (size[0], size[1])) + + # test only - frombuffer (default mode) is faster + if options.mode == 'string': + print('Using PIL fromstring method...') + else: + print('Using PIL frombuffer method...') + + # png format require rgb24 (24 bits) + media.addScaler(vstream, *size) + + decodedCount = 0 + # iterate over media + for pkt in media: + + if pkt.streamIndex() == vstream: + + # copy packet if required - test only + if options.copyPacket: + pkt2 = copy.copy(pkt) + else: + pkt2 = pkt + + pkt2.decode() + if pkt2.decoded: + + decodedCount += 1 + print('decoded frame %d' % decodedCount) + + if decodedCount >= options.offset: + print('saving frame...') + + buf = pkt2.swsFrame.contents.data[0] + + # width*height*3 (rgb24) + bufLen = size[0]*size[1]*3 + + if options.mode == 'string': + + surfaceStr = ctypes.string_at(buf, bufLen) + img = Image.fromstring('RGB', size, surfaceStr) + + elif options.mode == 'buffer': + + # benchmark note: + # it seems that using 'buffer' mode is not really faster than 'string' mode but + # to see changes, comment the i.convert('LA').save(...) line + + buf2 = ctypes.cast(buf, ctypes.POINTER(ctypes.c_ubyte*bufLen)) + img = Image.frombuffer('RGB', (size[0], size[1]) , buf2.contents, 'raw', 'RGB', 0, 1) + + # convert to grayscale then write png + img.convert('LA').save('frame.%d.png' % decodedCount) + + if decodedCount >= options.offset + options.frameCount: + break + diff --git a/examples/outputPygame/outputPygame.py b/examples/outputPygame/outputPygame.py index ad69669..f2e70f8 100755 --- a/examples/outputPygame/outputPygame.py +++ b/examples/outputPygame/outputPygame.py @@ -46,7 +46,7 @@ try: media = Media(options.media) except IOError as e: - print('Unable to open %s: %s' % (options.media, e)) + print(('Unable to open %s: %s' % (options.media, e))) sys.exit(1) # dump info @@ -57,15 +57,15 @@ if vstreams: vstream = vstreams[0] else: - print('No video stream in %s' % mediaInfo['name']) + print(('No video stream in %s' % mediaInfo['name'])) sys.exit(2) # retrieve video width and height streamInfo = mediaInfo['stream'][vstream] size = streamInfo['width'], streamInfo['height'] - print('video stream index: %d' % vstream) - print('video stream resolution: %dx%d' % (size[0], size[1])) + print(('video stream index: %d' % vstream)) + print(('video stream resolution: %dx%d' % (size[0], size[1]))) # pygame format require rgb24 (24 bits) media.addScaler(vstream, *size, scaling=options.scaling) @@ -86,7 +86,7 @@ if pkt2.decoded: decodedCount += 1 - print('decoded frame %d' % decodedCount) + print(('decoded frame %d' % decodedCount)) if decodedCount >= options.offset: print('saving frame...') diff --git a/examples/outputPygame/outputPygame.py.bak b/examples/outputPygame/outputPygame.py.bak new file mode 100755 index 0000000..ad69669 --- /dev/null +++ b/examples/outputPygame/outputPygame.py.bak @@ -0,0 +1,104 @@ +#!/usr/bin/env python + +''' +Decode the 5th first frame of the first video stream and write jpg (require pygame lib) + +python outputPygame.py -m file.avi -> save frame 1 to 5 +python outputPygame.py -m file.avi -o 140 -> save frame 140 to 145 +''' + +import sys +import ctypes +import copy + +import pygame + +from avpy import Media + +if __name__ == '__main__': + + # cmdline + from optparse import OptionParser + + usage = "usage: %prog -m foo.avi -o 140" + parser = OptionParser(usage=usage) + parser.add_option('-m', '--media', + help='play media') + parser.add_option('-o', '--offset', + type='int', + help='frame offset', default=0) + parser.add_option('-c', '--frameCount', + type='int', + help='number of image to save (default: %default)', default=5) + parser.add_option('--copyPacket', + action='store_true', + help='copy packet (debug only)') + + parser.add_option('--scaling', + default='bilinear', + help='scaling algorithm') + + (options, args) = parser.parse_args() + + if not options.media: + print('Please provide a media to play with -m or --media option') + sys.exit(1) + try: + media = Media(options.media) + except IOError as e: + print('Unable to open %s: %s' % (options.media, e)) + sys.exit(1) + + # dump info + mediaInfo = media.info() + + # select first video stream + vstreams = [ i for i, s in enumerate(mediaInfo['stream']) if s['type'] == 'video' ] + if vstreams: + vstream = vstreams[0] + else: + print('No video stream in %s' % mediaInfo['name']) + sys.exit(2) + + # retrieve video width and height + streamInfo = mediaInfo['stream'][vstream] + size = streamInfo['width'], streamInfo['height'] + + print('video stream index: %d' % vstream) + print('video stream resolution: %dx%d' % (size[0], size[1])) + + # pygame format require rgb24 (24 bits) + media.addScaler(vstream, *size, scaling=options.scaling) + + decodedCount = 0 + # iterate over media + for pkt in media: + + if pkt.streamIndex() == vstream: + + # copy packet if required - test only + if options.copyPacket: + pkt2 = copy.copy(pkt) + else: + pkt2 = pkt + + pkt2.decode() + if pkt2.decoded: + + decodedCount += 1 + print('decoded frame %d' % decodedCount) + + if decodedCount >= options.offset: + print('saving frame...') + + buf = pkt2.swsFrame.contents.data[0] + + # width*height*3 (rgb24) + bufLen = size[0]*size[1]*3 + surfaceStr = ctypes.string_at(buf, bufLen) + + surface = pygame.image.fromstring(surfaceStr, size, 'RGB') + pygame.image.save(surface, 'frame.%d.jpg' % decodedCount) + + if decodedCount >= options.offset + options.frameCount: + break diff --git a/examples/outputPygame/outputPygame2.py b/examples/outputPygame/outputPygame2.py index 9e4b420..931a6c1 100755 --- a/examples/outputPygame/outputPygame2.py +++ b/examples/outputPygame/outputPygame2.py @@ -48,7 +48,7 @@ try: media = Media(options.media) except IOError as e: - print('Unable to open %s: %s' % (options.media, e)) + print(('Unable to open %s: %s' % (options.media, e))) sys.exit(1) # dump info @@ -59,20 +59,20 @@ if vstreams: vstream = vstreams[0] else: - print('No video stream in %s' % mediaInfo['name']) + print(('No video stream in %s' % mediaInfo['name'])) sys.exit(2) # retrieve video width and height streamInfo = mediaInfo['stream'][vstream] size = streamInfo['width'], streamInfo['height'] - print('video stream index: %d' % vstream) - print('video stream resolution: %dx%d' % (size[0], size[1])) + print(('video stream index: %d' % vstream)) + print(('video stream resolution: %dx%d' % (size[0], size[1]))) size = ( int(round(size[0]*options.scaleWidth)), int(round(size[1]*options.scaleHeight)) ) - print('output resolution: %dx%d' % (size)) + print(('output resolution: %dx%d' % (size))) # setup pygame pygame.init() @@ -106,7 +106,7 @@ while mainLoop: try: - pkt = media.next() + pkt = next(media) except StopIteration: mainLoop = False continue diff --git a/examples/outputPygame/outputPygame2.py.bak b/examples/outputPygame/outputPygame2.py.bak new file mode 100755 index 0000000..9e4b420 --- /dev/null +++ b/examples/outputPygame/outputPygame2.py.bak @@ -0,0 +1,163 @@ +#!/usr/bin/env python + +''' +demo video player (pygame) +python outputPygame2.py -m file.avi + +.. note: + * use yuv hardware acceleration if available + * no sound + * no video sync +''' + +import sys +import ctypes +import copy + +import pygame + +from avpy import Media + +if __name__ == '__main__': + + # cmdline + from optparse import OptionParser + + usage = "usage: %prog -m foo.avi" + parser = OptionParser(usage=usage) + parser.add_option('-m', '--media', + help='play media') + parser.add_option('--copyPacket', + action='store_true', + help='copy packet (debug only)') + parser.add_option('--scaleWidth', + type='float', default=1.0, + help='width scale (default: %default)') + parser.add_option('--scaleHeight', + type='float', default=1.0, + help='height scale (default: %default)') + parser.add_option('-f', '--fullscreen', + action='store_true', + help='turn on full screen mode') + #parser.add_option('--scaling', + #default='bilinear', + #help='scaling algorithm') + + (options, args) = parser.parse_args() + + try: + media = Media(options.media) + except IOError as e: + print('Unable to open %s: %s' % (options.media, e)) + sys.exit(1) + + # dump info + mediaInfo = media.info() + + # select first video stream + vstreams = [i for i, s in enumerate(mediaInfo['stream']) if s['type'] == 'video'] + if vstreams: + vstream = vstreams[0] + else: + print('No video stream in %s' % mediaInfo['name']) + sys.exit(2) + + # retrieve video width and height + streamInfo = mediaInfo['stream'][vstream] + size = streamInfo['width'], streamInfo['height'] + + print('video stream index: %d' % vstream) + print('video stream resolution: %dx%d' % (size[0], size[1])) + + size = ( int(round(size[0]*options.scaleWidth)), + int(round(size[1]*options.scaleHeight)) ) + + print('output resolution: %dx%d' % (size)) + + # setup pygame + pygame.init() + + if options.fullscreen: + screen = pygame.display.set_mode(size, pygame.DOUBLEBUF|pygame.HWSURFACE|pygame.FULLSCREEN) + else: + screen = pygame.display.set_mode(size) + + useYuv = False + if streamInfo['pixelFormat'] == 'yuv420p': + overlay = pygame.Overlay(pygame.YV12_OVERLAY, size) + overlay.set_location(0, 0, size[0], size[1]) + + useYuv = True + if overlay.get_hardware(): + print('render: Hardware accelerated yuv overlay (fast)') + else: + print('render: Software yuv overlay (slow)') + else: + print('render: software rgb (very slow)') + # add scaler to convert to rgb + media.addScaler(vstream, *size) + #media.addScaler(vstream, *size, scaling='gauss') + + decodedCount = 0 + mainLoop = True + + print('Press Esc to quit...') + + while mainLoop: + + try: + pkt = media.next() + except StopIteration: + mainLoop = False + continue + + if pkt.streamIndex() == vstream: + + if options.copyPacket: + pkt2 = copy.copy(pkt) + else: + pkt2 = pkt + + pkt2.decode() + if pkt2.decoded: + decodedCount += 1 + + if useYuv: + + # upload yuv data + + size0 = pkt2.frame.contents.linesize[0] * pkt2.frame.contents.height + size1 = pkt2.frame.contents.linesize[1] * (pkt2.frame.contents.height//2) + size2 = pkt2.frame.contents.linesize[2] * (pkt2.frame.contents.height//2) + + yuv = (ctypes.string_at(pkt2.frame.contents.data[0], size0), + ctypes.string_at(pkt2.frame.contents.data[1], size1), + ctypes.string_at(pkt2.frame.contents.data[2], size2)) + + overlay.display(yuv) + + # add a small delay otherwise pygame will crash + pygame.time.wait(20) + + else: + + # upload rgb data + + buf = pkt2.swsFrame.contents.data[0] + bufLen = size[0]*size[1]*3 + surfaceStr = ctypes.string_at(buf, bufLen) + cSurface = pygame.image.fromstring(surfaceStr, size, 'RGB') + + pygame.display.set_caption('Press Esc to quit...') + screen.blit(cSurface, (0, 0)) + + pygame.display.flip() + + # event processing + for event in pygame.event.get(): + if event.type == pygame.QUIT: + mainLoop = False + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + mainLoop = False + diff --git a/examples/outputSDL2/outputBMP.py b/examples/outputSDL2/outputBMP.py index 4d3b8f7..48f9054 100644 --- a/examples/outputSDL2/outputBMP.py +++ b/examples/outputSDL2/outputBMP.py @@ -43,7 +43,7 @@ try: media = Media(options.media) except IOError as e: - print('Unable to open %s: %s' % (options.media, e)) + print(('Unable to open %s: %s' % (options.media, e))) sys.exit(1) # dump info @@ -54,15 +54,15 @@ if vstreams: vstream = vstreams[0] else: - print('No video stream in %s' % mediaInfo['name']) + print(('No video stream in %s' % mediaInfo['name'])) sys.exit(2) # retrieve video width and height streamInfo = mediaInfo['stream'][vstream] size = streamInfo['width'], streamInfo['height'] - print('video stream index: %d' % vstream) - print('video stream resolution: %dx%d' % (size[0], size[1])) + print(('video stream index: %d' % vstream)) + print(('video stream resolution: %dx%d' % (size[0], size[1]))) # bmp format require rgb24 (24 bits) media.addScaler(vstream, *size) @@ -71,10 +71,10 @@ # sdl2 version - debug only sdl2Version = sdl2.version_info - print('Using sdl2 version: %d.%d.%d' % - (sdl2Version[0], sdl2Version[1], sdl2Version[2])) + print(('Using sdl2 version: %d.%d.%d' % + (sdl2Version[0], sdl2Version[1], sdl2Version[2]))) if sdl2.SDL_Init(0) != 0: - print(sdl2.SDL_GetError()) + print((sdl2.SDL_GetError())) sys.exit(3) if sdl2.SDL_BYTEORDER == sdl2.SDL_LIL_ENDIAN: @@ -104,7 +104,7 @@ if pkt2.decoded: decodedCount += 1 - print('decoded frame %d' % decodedCount) + print(('decoded frame %d' % decodedCount)) if decodedCount >= options.offset: print('saving frame...') diff --git a/examples/outputSDL2/outputBMP.py.bak b/examples/outputSDL2/outputBMP.py.bak new file mode 100644 index 0000000..4d3b8f7 --- /dev/null +++ b/examples/outputSDL2/outputBMP.py.bak @@ -0,0 +1,126 @@ +#!/usr/bin/env python + +''' +Decode the 5th first frame of the first video stream and write bmp (require PySDL2) + +python outputBMP.py -m file.avi -> save frame 1 to 5 +python outputBMP.py -m file.avi -o 140 -> save frame 140 to 145 +''' + +import sys +import ctypes +import copy + +import sdl2 + +from avpy import Media + +if __name__ == '__main__': + + # cmdline + from optparse import OptionParser + + usage = "usage: %prog -m foo.avi -o 140" + parser = OptionParser(usage=usage) + parser.add_option('-m', '--media', + help='play media') + parser.add_option('-o', '--offset', + type='int', + help='frame offset', default=0) + parser.add_option('-c', '--frameCount', + type='int', + help='number of image to save (default: %default)', default=5) + parser.add_option('--copyPacket', + action='store_true', + help='copy packet (debug only)') + + (options, args) = parser.parse_args() + + if not options.media: + print('Please provide a media to play with -m or --media option') + sys.exit(1) + + try: + media = Media(options.media) + except IOError as e: + print('Unable to open %s: %s' % (options.media, e)) + sys.exit(1) + + # dump info + mediaInfo = media.info() + + # select first video stream + vstreams = [ i for i, s in enumerate(mediaInfo['stream']) if s['type'] == 'video' ] + if vstreams: + vstream = vstreams[0] + else: + print('No video stream in %s' % mediaInfo['name']) + sys.exit(2) + + # retrieve video width and height + streamInfo = mediaInfo['stream'][vstream] + size = streamInfo['width'], streamInfo['height'] + + print('video stream index: %d' % vstream) + print('video stream resolution: %dx%d' % (size[0], size[1])) + + # bmp format require rgb24 (24 bits) + media.addScaler(vstream, *size) + + # setup sdl2 + + # sdl2 version - debug only + sdl2Version = sdl2.version_info + print('Using sdl2 version: %d.%d.%d' % + (sdl2Version[0], sdl2Version[1], sdl2Version[2])) + if sdl2.SDL_Init(0) != 0: + print(sdl2.SDL_GetError()) + sys.exit(3) + + if sdl2.SDL_BYTEORDER == sdl2.SDL_LIL_ENDIAN: + rmask = 0x000000ff + gmask = 0x0000ff00 + bmask = 0x00ff0000 + amask = 0xff000000 + else: + rmask = 0xff000000 + gmask = 0x00ff0000 + bmask = 0x0000ff00 + amask = 0x000000ff + + decodedCount = 0 + # iterate over media + for pkt in media: + + if pkt.streamIndex() == vstream: + + # test only + if options.copyPacket: + pkt2 = copy.copy(pkt) + else: + pkt2 = pkt + + pkt2.decode() + if pkt2.decoded: + + decodedCount += 1 + print('decoded frame %d' % decodedCount) + + if decodedCount >= options.offset: + print('saving frame...') + + buf = pkt2.swsFrame.contents.data[0] + + # convert buffer to a SDL Surface + surface = sdl2.SDL_CreateRGBSurfaceFrom(buf, + size[0], size[1], 24, + size[0] * 3, + rmask, gmask, + bmask, amask) + # save then free surface + sdl2.SDL_SaveBMP(surface, 'frame.%d.bmp' % decodedCount) + sdl2.SDL_FreeSurface(surface) + + if decodedCount >= options.offset + options.frameCount: + break + diff --git a/examples/outputSDL2/outputSDL2.py b/examples/outputSDL2/outputSDL2.py index bc2248c..55d19df 100644 --- a/examples/outputSDL2/outputSDL2.py +++ b/examples/outputSDL2/outputSDL2.py @@ -49,7 +49,7 @@ try: media = Media(options.media) except IOError as e: - print('Unable to open %s: %s' % (options.media, e)) + print(('Unable to open %s: %s' % (options.media, e))) sys.exit(1) # dump info @@ -60,27 +60,27 @@ if vstreams: vstream = vstreams[0] else: - print('No video stream in %s' % mediaInfo['name']) + print(('No video stream in %s' % mediaInfo['name'])) sys.exit(2) streamInfo = mediaInfo['stream'][vstream] size = streamInfo['width'], streamInfo['height'] - print('video stream index: %d' % vstream) - print('video stream resolution: %dx%d' % (size[0], size[1])) + print(('video stream index: %d' % vstream)) + print(('video stream resolution: %dx%d' % (size[0], size[1]))) size = ( int(round(size[0]*options.scaleWidth)), int(round(size[1]*options.scaleHeight)) ) - print('output resolution: %dx%d' % (size)) + print(('output resolution: %dx%d' % (size))) # setup sdl2 sdl2Version = sdl2.version_info - print('Using sdl2 version: %d.%d.%d' % - (sdl2Version[0], sdl2Version[1], sdl2Version[2])) + print(('Using sdl2 version: %d.%d.%d' % + (sdl2Version[0], sdl2Version[1], sdl2Version[2]))) if sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO) != 0: - print(sdl2.SDL_GetError()) + print((sdl2.SDL_GetError())) sys.exit(3) window = sdl2.SDL_CreateWindow(b'Demo player', @@ -89,7 +89,7 @@ size[0], size[1], sdl2.SDL_WINDOW_SHOWN) if not window: - print(sdl2.SDL_GetError()) + print((sdl2.SDL_GetError())) sys.exit(4) if sdl2.SDL_BYTEORDER == sdl2.SDL_LIL_ENDIAN: @@ -113,7 +113,7 @@ # a more valid example would check for renderer supported resolution if not renderer: - print('Could not create renderer: %s' % sdl2.SDL_GetError()) + print(('Could not create renderer: %s' % sdl2.SDL_GetError())) useTexture = False else: useTexture = True @@ -123,15 +123,15 @@ if res == 0: formats = rendererInfo.texture_formats - print('renderer %s - fmts:' % rendererInfo.name) + print(('renderer %s - fmts:' % rendererInfo.name)) for fmt in formats: - print(sdl2.SDL_GetPixelFormatName(fmt)) + print((sdl2.SDL_GetPixelFormatName(fmt))) if sdl2.SDL_PIXELFORMAT_YV12 in formats: rendererYuv = True else: - print('Warning: render (%s) does not support yv12 format' - % rendererInfo.name) + print(('Warning: render (%s) does not support yv12 format' + % rendererInfo.name)) # XXX: opengl renderer does not report supporting RGB24 # but only ARGB888 - bug? @@ -141,10 +141,10 @@ rendererRgb = True else: # wtf? - print('Warning: renderer (%s) does not support rgb24 or argb8888 format' - % rendererInfo.name) + print(('Warning: renderer (%s) does not support rgb24 or argb8888 format' + % rendererInfo.name)) else: - print('Unable to query render info: %s' % sdl2.SDL_GetError()) + print(('Unable to query render info: %s' % sdl2.SDL_GetError())) useYuv = False @@ -206,7 +206,7 @@ running = False break - pkt = media.next() + pkt = next(media) if pkt.streamIndex() == vstream: pkt.decode() diff --git a/examples/outputSDL2/outputSDL2.py.bak b/examples/outputSDL2/outputSDL2.py.bak new file mode 100644 index 0000000..bc2248c --- /dev/null +++ b/examples/outputSDL2/outputSDL2.py.bak @@ -0,0 +1,262 @@ +#!/usr/bin/env python + +''' +demo video player (PySDL2) +python outputSDL2.py -m file.avi + +.. note: + * use yuv hardware acceleration if available + * no sound + * no video sync + +.. warning: + * this is a complex and low level example, + see outputPygame2 for a simpler player +''' + +import sys +import ctypes +import copy + +import sdl2 + +from avpy import Media + +if __name__ == '__main__': + + # cmdline + from optparse import OptionParser + + usage = "usage: %prog -m foo.avi" + parser = OptionParser(usage=usage) + parser.add_option('-m', '--media', + help='play media') + parser.add_option('--copyPacket', + action='store_true', + help='copy packet (debug only)') + parser.add_option('--scaleWidth', + type='float', default=1.0, + help='width scale (default: %default)') + parser.add_option('--scaleHeight', + type='float', default=1.0, + help='height scale (default: %default)') + parser.add_option('-f', '--fullscreen', + action='store_true', + help='turn on full screen mode') + + (options, args) = parser.parse_args() + + try: + media = Media(options.media) + except IOError as e: + print('Unable to open %s: %s' % (options.media, e)) + sys.exit(1) + + # dump info + mediaInfo = media.info() + + # select first video stream + vstreams = [i for i, s in enumerate(mediaInfo['stream']) if s['type'] == 'video'] + if vstreams: + vstream = vstreams[0] + else: + print('No video stream in %s' % mediaInfo['name']) + sys.exit(2) + + streamInfo = mediaInfo['stream'][vstream] + size = streamInfo['width'], streamInfo['height'] + + print('video stream index: %d' % vstream) + print('video stream resolution: %dx%d' % (size[0], size[1])) + + size = ( int(round(size[0]*options.scaleWidth)), + int(round(size[1]*options.scaleHeight)) ) + + print('output resolution: %dx%d' % (size)) + + # setup sdl2 + sdl2Version = sdl2.version_info + print('Using sdl2 version: %d.%d.%d' % + (sdl2Version[0], sdl2Version[1], sdl2Version[2])) + + if sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO) != 0: + print(sdl2.SDL_GetError()) + sys.exit(3) + + window = sdl2.SDL_CreateWindow(b'Demo player', + sdl2.SDL_WINDOWPOS_CENTERED, + sdl2.SDL_WINDOWPOS_CENTERED, + size[0], size[1], + sdl2.SDL_WINDOW_SHOWN) + if not window: + print(sdl2.SDL_GetError()) + sys.exit(4) + + if sdl2.SDL_BYTEORDER == sdl2.SDL_LIL_ENDIAN: + rmask = 0x000000ff + gmask = 0x0000ff00 + bmask = 0x00ff0000 + amask = 0xff000000 + else: + rmask = 0xff000000 + gmask = 0x00ff0000 + bmask = 0x0000ff00 + amask = 0x000000ff + + renderer = sdl2.SDL_CreateRenderer(window, -1, sdl2.SDL_RENDERER_ACCELERATED) + + useTexture = False + rendererYuv = False + rendererRgb = True # force to True (see below) + + # query renderer caps + # a more valid example would check for renderer supported resolution + + if not renderer: + print('Could not create renderer: %s' % sdl2.SDL_GetError()) + useTexture = False + else: + useTexture = True + rendererInfo = sdl2.SDL_RendererInfo() + res = sdl2.SDL_GetRendererInfo(renderer, rendererInfo) + + if res == 0: + formats = rendererInfo.texture_formats + + print('renderer %s - fmts:' % rendererInfo.name) + for fmt in formats: + print(sdl2.SDL_GetPixelFormatName(fmt)) + + if sdl2.SDL_PIXELFORMAT_YV12 in formats: + rendererYuv = True + else: + print('Warning: render (%s) does not support yv12 format' + % rendererInfo.name) + + # XXX: opengl renderer does not report supporting RGB24 + # but only ARGB888 - bug? + + if sdl2.SDL_PIXELFORMAT_RGB24 in formats or\ + sdl2.SDL_PIXELFORMAT_ARGB8888 in formats: + rendererRgb = True + else: + # wtf? + print('Warning: renderer (%s) does not support rgb24 or argb8888 format' + % rendererInfo.name) + else: + print('Unable to query render info: %s' % sdl2.SDL_GetError()) + + useYuv = False + + # 3 rendering modes: + # mode 1: useYuv + useTexture == True + # -> upload yuv data to graphic card + hardware scaling + # mode 2: useTexture == True, useYuv == False + # -> yuv to rgb in software, upload rgb to graphic card + hardware scaling + # mode 3: useTexture + useYuv == False + # -> uyv to rgb + scaling in software + # note: in this example, we don't perform scaling + + if useTexture == True: + + # select clear color + sdl2.SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255) + # clear screen + sdl2.SDL_RenderClear(renderer) + sdl2.SDL_RenderPresent(renderer) + + # mode 1 + if rendererYuv == True and streamInfo['pixelFormat'] == 'yuv420p': + + print('mode 1: fast yuv rendering...') + + useYuv = True + + # texture will change frequently so use streaming access + yuvTexture = sdl2.SDL_CreateTexture(renderer, sdl2.SDL_PIXELFORMAT_YV12, + sdl2.SDL_TEXTUREACCESS_STREAMING, size[0], size[1]) + else: + + print('mode 2: basic rgb rendering (slow)...') + + # mode 2 + m.addScaler(vstream, *size) + + else: + + print('mode 3: software rendering (very slow)...') + + # mode 3 + m.addScaler(vstream, *size) + + sdl2.SDL_DestroyRenderer(renderer) + windowSurface = sdl2.SDL_GetWindowSurface(window) + + print('Press Esc to quit...') + + running = True + event = sdl2.SDL_Event() + while running: + + # process events... + while sdl2.SDL_PollEvent(ctypes.byref(event)) != 0: + keySim = event.key.keysym.sym + if (event.type == sdl2.SDL_KEYDOWN and keySim == sdl2.SDLK_ESCAPE) or\ + event.type == sdl2.SDL_QUIT: + running = False + break + + pkt = media.next() + + if pkt.streamIndex() == vstream: + pkt.decode() + if pkt.decoded: + + if useYuv: + + # upload yuv data + + frameData = pkt.frame.contents + + sdl2.SDL_UpdateYUVTexture(yuvTexture, None, + frameData.data[0], frameData.linesize[0], + frameData.data[1], frameData.linesize[1], + frameData.data[2], frameData.linesize[2] ) + + res = sdl2.SDL_RenderCopy(renderer, yuvTexture, None, None); + if res < 0: + raise RuntimeError(sdl2.SDL_GetError()) + sdl2.SDL_RenderPresent(renderer) + + else: + + # upload rgb data + + buf = pkt.swsFrame.contents.data[0] + surface = sdl2.SDL_CreateRGBSurfaceFrom(buf, + size[0], size[1], 24, + size[0] * 3, + rmask, gmask, + bmask, amask) + + #print sdl2.SDL_GetPixelFormatName(surface.contents.format.contents.format) + + if useTexture: + + texture = sdl2.SDL_CreateTextureFromSurface(renderer, surface) + + res = sdl2.SDL_RenderCopy(renderer, texture, None, None) + if res < 0: + raise RuntimeError(sdl2.SDL_GetError()) + sdl2.SDL_RenderPresent(renderer) + + else: + sdl2.SDL_BlitSurface(surface, None, windowSurface, None) + sdl2.SDL_UpdateWindowSurface(window) + sdl2.SDL_FreeSurface(surface) + + sdl2.SDL_Delay(10) + + sdl2.SDL_DestroyWindow(window) + sdl2.SDL_Quit() + diff --git a/examples/outputWav/outputWav.py b/examples/outputWav/outputWav.py index 5996884..4f1a46a 100755 --- a/examples/outputWav/outputWav.py +++ b/examples/outputWav/outputWav.py @@ -74,7 +74,7 @@ def writeWav(wp): if astreams: astream = astreams[0] else: - print('No audio stream in %s' % mediaInfo['name']) + print(('No audio stream in %s' % mediaInfo['name'])) sys.exit(2) astreamInfo = mediaInfo['stream'][astream] @@ -106,7 +106,7 @@ def writeWav(wp): media.addResampler(astream, inAudio, outAudio) except RuntimeError as e: - print('Could not add an audio resampler: %s' % e) + print(('Could not add an audio resampler: %s' % e)) sys.exit(3) else: @@ -124,7 +124,7 @@ def writeWav(wp): 'NONE', 'not compressed') ) except wave.Error as e: - print('Wrong parameters for wav file: %s' % e) + print(('Wrong parameters for wav file: %s' % e)) sys.exit(1) # Size in bytes required for 1 second of audio @@ -138,7 +138,7 @@ def writeWav(wp): if pkt.streamIndex() == astream: pkt.decode() if pkt.decoded: - print('writing %s bytes...' % pkt.dataSize) + print(('writing %s bytes...' % pkt.dataSize)) if resampler: audioDump(pkt.resampledFrame.contents.data[0], @@ -156,5 +156,5 @@ def writeWav(wp): # all audio data have been decoded - write file writeWav(wp) - print('writing %s done!' % outputName) + print(('writing %s done!' % outputName)) diff --git a/examples/outputWav/outputWav.py.bak b/examples/outputWav/outputWav.py.bak new file mode 100755 index 0000000..5996884 --- /dev/null +++ b/examples/outputWav/outputWav.py.bak @@ -0,0 +1,160 @@ +#!/usr/bin/env python + +''' +Decode 20 seconds of the first audio stream +and output it into a wav file +''' + +import sys +import struct +import wave + +from avpy import avMedia, Media + +# wav data (see audioDump) +waveData = [] + +def audioDump2(buf, bufLen): + + ''' Store audio data + ''' + + # TODO: unused, remove? + + global waveData + for i in range(bufLen): + waveData.append(struct.pack('B', buf[i])) + +def audioDump(buf, bufLen): + + ''' Store audio data + + .. note:: faster than audioDump2 + ''' + + import ctypes + global waveData + waveData.append(ctypes.string_at(buf, bufLen)) + +def writeWav(wp): + + ''' Write wav file + ''' + + global waveData + + sep = ''.encode('ascii') + + # write data to wav object + wp.writeframes(sep.join(waveData)) + wp.close() + +if __name__ == '__main__': + + # cmdline + from optparse import OptionParser + + usage = "usage: %prog -m foo.avi" + parser = OptionParser(usage=usage) + parser.add_option('-m', '--media', + help='play media') + parser.add_option('-l', '--length', + help='decode at max seconds of audio', + type='int', + default=20) + + (options, args) = parser.parse_args() + + media = Media(options.media) + # dump info + mediaInfo = media.info() + + # select first audio stream + astreams = [ i for i, s in enumerate(mediaInfo['stream']) if s['type'] == 'audio' ] + if astreams: + astream = astreams[0] + else: + print('No audio stream in %s' % mediaInfo['name']) + sys.exit(2) + + astreamInfo = mediaInfo['stream'][astream] + + # forge output audio format + # write a standard stereo 16 bits 44.1 kHz wav file + + # only force sample rate resampling if supported + # libav 0.8 does not support audio resampling + # outSampleRate = astreamInfo['sampleRate'] + outSampleRate = 44100 if avMedia.AVPY_RESAMPLE_SUPPORT else astreamInfo['sampleRate'] + outAudio = { + 'layout': 'stereo', # XXX: channelLayout? + 'channels': 2, + 'sampleRate': outSampleRate, + 'sampleFmt': 's16', + 'bytesPerSample': 2, + } + + if outAudio['layout'] != astreamInfo['channelLayout'] or\ + outAudio['channels'] != astreamInfo['channels'] or\ + outAudio['sampleFmt'] != astreamInfo['sampleFmt'] or\ + outAudio['sampleRate'] != astreamInfo['sampleRate']: + + inAudio = astreamInfo + resampler = True + + try: + media.addResampler(astream, inAudio, outAudio) + except RuntimeError as e: + + print('Could not add an audio resampler: %s' % e) + sys.exit(3) + + else: + resampler = False + + # setup output wav file + outputName = 'out.wav' + wp = wave.open(outputName, 'w') + try: + # nchannels, sampwidth, framerate, nframes, comptype, compname + wp.setparams( (outAudio['channels'], + outAudio['bytesPerSample'], + outAudio['sampleRate'], + 0, + 'NONE', + 'not compressed') ) + except wave.Error as e: + print('Wrong parameters for wav file: %s' % e) + sys.exit(1) + + # Size in bytes required for 1 second of audio + # secondSize = astreamInfo['channels'] * astreamInfo['bytesPerSample'] * astreamInfo['sampleRate'] + secondSize = outAudio['channels'] * outAudio['bytesPerSample'] * outAudio['sampleRate'] + decodedSize = 0 + + # iterate over media and decode audio packet + for pkt in media: + + if pkt.streamIndex() == astream: + pkt.decode() + if pkt.decoded: + print('writing %s bytes...' % pkt.dataSize) + + if resampler: + audioDump(pkt.resampledFrame.contents.data[0], + pkt.rDataSize) + decodedSize += pkt.rDataSize + else: + audioDump(pkt.frame.contents.data[0], + pkt.dataSize) + decodedSize += pkt.dataSize + + # stop after ~ 20s (default) + # exact time will vary depending on dataSize + if decodedSize >= options.length*secondSize: + break + + # all audio data have been decoded - write file + writeWav(wp) + print('writing %s done!' % outputName) + diff --git a/tests/test_outputAudio.py b/tests/test_outputAudio.py index 98f39b2..1455cd1 100644 --- a/tests/test_outputAudio.py +++ b/tests/test_outputAudio.py @@ -112,11 +112,11 @@ def testAudioResampling(self): assert self.compareAudioData(frame, pkt.rDataSize) expectedSize = duration*secondSize - print('decodedSize', decodedSize) - print('expectedSize', expectedSize) - print('frame size', streamInfo['frameSize']) + print(('decodedSize', decodedSize)) + print(('expectedSize', expectedSize)) + print(('frame size', streamInfo['frameSize'])) secondsDiff = float(expectedSize-decodedSize)/secondSize - print('diff (in seconds)', secondsDiff) + print(('diff (in seconds)', secondsDiff)) # XXX: accept a small margin error for now # wondering if this is normal or not... assert secondsDiff < 0.01 diff --git a/tests/test_outputAudio.py.bak b/tests/test_outputAudio.py.bak new file mode 100644 index 0000000..98f39b2 --- /dev/null +++ b/tests/test_outputAudio.py.bak @@ -0,0 +1,143 @@ +import os +import copy +import array +import ctypes +import wave + +from avpy import avMedia + +class TestAudio(object): + + def compareAudioData(self, frame, dataSize): + + # 16 bits data + _a = array.array('B', [1, 0, 2, 0]*int(dataSize/4)) + a = array.array('B', [0]*(dataSize)) + + ptr = frame.contents.data[0] + ctypes.memmove(a.buffer_info()[0], ptr, dataSize) + if _a != a: + return False + + return True + + def testAudio(self): + + mediaName = os.environ['CONSTANT_WAV'] + media = avMedia.Media(mediaName, quiet=False) + mediaInfo = media.info() + + astream = 0 # audio stream index + streamInfo = mediaInfo['stream'][astream] + + wp = wave.open(mediaName) + + # check ffmpeg info against wave info + frames = wp.getnframes() + rate = wp.getframerate() + assert wp.getnchannels() == streamInfo['channels'] + assert rate == streamInfo['sampleRate'] + assert wp.getsampwidth() == streamInfo['bytesPerSample'] + duration = frames / float(rate) + assert duration == mediaInfo['duration'] + + # size in bytes for 1 second of audio + secondSize = streamInfo['channels'] * streamInfo['bytesPerSample'] * streamInfo['sampleRate'] + + decodedSize = 0 + for pkt in media: + if pkt.streamIndex() == astream: + pkt.decode() + if pkt.decoded: + + frame = pkt.frame + decodedSize += pkt.dataSize + assert self.compareAudioData(frame, pkt.dataSize) + + # check total decoded size + # TODO: check with wav of len 2.1s + assert secondSize * duration == decodedSize + + def testAudioResampling(self): + + mediaName = os.environ['CONSTANT_WAV'] + media = avMedia.Media(mediaName, quiet=False) + mediaInfo = media.info() + + astream = 0 # audio stream index + streamInfo = mediaInfo['stream'][astream] + + if not avMedia.AVPY_RESAMPLE_SUPPORT: + + print('No resampling support, test disabled.') + return + + wp = wave.open(mediaName) + + # check ffmpeg info against wave info + frames = wp.getnframes() + rate = wp.getframerate() + assert wp.getnchannels() == streamInfo['channels'] + assert rate == streamInfo['sampleRate'] + assert wp.getsampwidth() == streamInfo['bytesPerSample'] + duration = frames / float(rate) + assert duration == mediaInfo['duration'] + + # inital sample rate / 2 + outSampleRate = 22050 + outAudio = { + 'layout': 'stereo', # XXX: channelLayout? + 'channels': 2, + 'sampleRate': outSampleRate, + 'sampleFmt': 's16', + 'bytesPerSample': 2, + } + + hasResampler = media.addResampler(astream, streamInfo, outAudio) + + assert hasResampler + + # size in bytes for 1 second of audio + secondSize = outAudio['channels'] * outAudio['bytesPerSample'] * outAudio['sampleRate'] + + decodedSize = 0 + for pkt in media: + if pkt.streamIndex() == astream: + + pkt.decode() + if pkt.decoded: + + frame = pkt.resampledFrame + decodedSize += pkt.rDataSize + assert self.compareAudioData(frame, pkt.rDataSize) + + expectedSize = duration*secondSize + print('decodedSize', decodedSize) + print('expectedSize', expectedSize) + print('frame size', streamInfo['frameSize']) + secondsDiff = float(expectedSize-decodedSize)/secondSize + print('diff (in seconds)', secondsDiff) + # XXX: accept a small margin error for now + # wondering if this is normal or not... + assert secondsDiff < 0.01 + + def testCopyPacket(self): + + mediaName = os.environ['CONSTANT_WAV'] + media = avMedia.Media(mediaName, quiet=False) + mediaInfo = media.info() + + astream = 0 # audio stream index + streamInfo = mediaInfo['stream'][astream] + + for pkt in media: + if pkt.streamIndex() == astream: + + pkt2 = copy.copy(pkt) + + pkt2.decode() + if pkt2.decoded: + + frame = pkt2.frame + assert self.compareAudioData(frame, pkt2.dataSize) + diff --git a/tools/build.py b/tools/build.py index e51046b..b7cae0d 100755 --- a/tools/build.py +++ b/tools/build.py @@ -40,18 +40,18 @@ def buildGit(options): installFolder = os.path.abspath(os.path.join(options.repo, '%s_%s' % (options.lib, options.version))) if os.path.isdir(installFolder): - print('A build already exists @ %s' % installFolder) + print(('A build already exists @ %s' % installFolder)) sys.exit(1) else: os.makedirs(installFolder) if not os.path.isdir(pathToGit): # TODO: auto clone repo - print('Could not find folder %s, please clone %s before running build' % (pathToGit, options.lib)) + print(('Could not find folder %s, please clone %s before running build' % (pathToGit, options.lib))) sys.exit(1) - print('Building in %s' % pathToGit) - print('Install in %s' % installFolder) + print(('Building in %s' % pathToGit)) + print(('Install in %s' % installFolder)) os.chdir(pathToGit) @@ -73,8 +73,8 @@ def buildGit(options): run('make install > {0}/make_install.log 2>&1'.format(installFolder)) print('build done!') - print('configure log: %s' % os.path.join(installFolder, 'config.log')) - print('make log: %s' % os.path.join(installFolder, 'make.log')) + print(('configure log: %s' % os.path.join(installFolder, 'config.log'))) + print(('make log: %s' % os.path.join(installFolder, 'make.log'))) if options.doc: @@ -85,14 +85,14 @@ def buildGit(options): if not os.path.isfile(os.path.join(basedir, basefile)): basedir = 'doc' if not os.path.isfile(os.path.join(basedir, basefile)): - print('Could not find doxygen configuration file: %s' % basefile) + print(('Could not find doxygen configuration file: %s' % basefile)) sys.exit(2) doxyf = os.path.join(basedir, basefile) doxyDst = os.path.join(basedir, 'doxy', 'html') try: # clean - print('removing %s...' % doxyDst) + print(('removing %s...' % doxyDst)) shutil.rmtree(doxyDst) print('done.') except OSError as e: diff --git a/tools/build.py.bak b/tools/build.py.bak new file mode 100755 index 0000000..e51046b --- /dev/null +++ b/tools/build.py.bak @@ -0,0 +1,154 @@ +#!/usr/bin/env python + +''' +Easily build libav/ffmpeg from git + +howto: + +- clone ffmpeg or libav in BUILD_FOLDER +- cd tools +- run: + - ./build.py -l ffmpeg -v 2.5 -r BUILD_FOLDER + - ./build.py -l libav -v 0.8.1 -r BUILD_FOLDER +''' + +import os +import errno +import sys +import subprocess +import shutil + + +def run(cmd): + + ''' print and run shell command + ''' + + print(cmd) + + retCode = subprocess.call(cmd, shell=True) + if retCode != 0: + raise RuntimeError('cmd %s failed!' % cmd) + + +def buildGit(options): + + ''' build libav or ffmpeg git + ''' + + pathToGit = os.path.abspath(os.path.join(options.repo, '%s_git' % options.lib)) + installFolder = os.path.abspath(os.path.join(options.repo, '%s_%s' % (options.lib, options.version))) + + if os.path.isdir(installFolder): + print('A build already exists @ %s' % installFolder) + sys.exit(1) + else: + os.makedirs(installFolder) + + if not os.path.isdir(pathToGit): + # TODO: auto clone repo + print('Could not find folder %s, please clone %s before running build' % (pathToGit, options.lib)) + sys.exit(1) + + print('Building in %s' % pathToGit) + print('Install in %s' % installFolder) + + os.chdir(pathToGit) + + if options.lib == 'ffmpeg': + tagChar = 'n' + else: + tagChar = 'v' + + # compile is a std: ./configure && make && make install process + + run('git checkout %s%s' % (tagChar, options.version)) + run('git describe --tags') + run('./configure --prefix={0} {1} --logfile={0}/config.log'.format(installFolder, ' '.join(c for c in options.configure_options))) + + # clean up before building + run('make clean') + print('now building...') + run('make -j {1} -k > {0}/make.log 2>&1'.format(installFolder, options.jobs)) + run('make install > {0}/make_install.log 2>&1'.format(installFolder)) + + print('build done!') + print('configure log: %s' % os.path.join(installFolder, 'config.log')) + print('make log: %s' % os.path.join(installFolder, 'make.log')) + + if options.doc: + + # libav has Doxyfile in git root folder + # ffmpeg has Doxyfile in GIT_ROOT/doc + basedir = '' + basefile = 'Doxyfile' + if not os.path.isfile(os.path.join(basedir, basefile)): + basedir = 'doc' + if not os.path.isfile(os.path.join(basedir, basefile)): + print('Could not find doxygen configuration file: %s' % basefile) + sys.exit(2) + + doxyf = os.path.join(basedir, basefile) + doxyDst = os.path.join(basedir, 'doxy', 'html') + try: + # clean + print('removing %s...' % doxyDst) + shutil.rmtree(doxyDst) + print('done.') + except OSError as e: + if e.errno == errno.ENOENT: + pass + else: + raise + + # gen + run('doxygen %s > %s 2>&1' % (doxyf, os.path.join(installFolder, 'doxygen.log'))) + # pseudo install + run('cp -r {0} {1}'.format(doxyDst, installFolder)) + + +def main(options): + + buildGit(options) + + +if __name__ == '__main__': + + from optparse import OptionParser + + usage = 'build.py -l libav -v 0.8.1' + parser = OptionParser(usage=usage) + parser.add_option('-l', '--lib', + help='lib to build: ffmpeg or libav (default: %default)', + default='libav') + parser.add_option('-v', '--version', + help='libav version to build (ex: 0.8.1)') + parser.add_option('-d', '--doc', + action='store_true', + default=False, + help='generate documentation (require doxygen)') + parser.add_option('-c', '--configure_options', + action='append', + default=['--enable-shared', '--enable-swscale'], + help='configure options (default: %default)' + ) + parser.add_option('-s', '--suffix', + default='', + help='install folder suffix, ex: libav_0.8.1_SUFFIX' + ) + parser.add_option('-j', '--jobs', + default=2, + type='int', + help='allow N jobs at once (default: %default)' + ) + parser.add_option('-r', '--repo', + default='./build', + help='where to find libav_git or ffmpeg_git folder') + + (options, args) = parser.parse_args() + + if not options.version: + print('Please provide a version using -v flag') + else: + main(options) + diff --git a/tools/fix.py b/tools/fix.py index 126e650..c2a8619 100755 --- a/tools/fix.py +++ b/tools/fix.py @@ -46,7 +46,7 @@ dcMap[rgx.group(2)] = cls fixCount = 0 - print('writing %s...' % options.dst) + print(('writing %s...' % options.dst)) with open(options.src, 'r') as sfp: with open(options.dst, 'w') as dfp: for line in sfp: @@ -64,5 +64,5 @@ def replClass(dcMap, count, matchobj): dfp.write(nline) - print('%d line(s) fixed!' % fixCount) + print(('%d line(s) fixed!' % fixCount)) diff --git a/tools/fix.py.bak b/tools/fix.py.bak new file mode 100755 index 0000000..126e650 --- /dev/null +++ b/tools/fix.py.bak @@ -0,0 +1,68 @@ +#!/usr/bin/env python + +''' +Burn special structure according to config + +gen.py will generate some dynamic class like: +- N8AVPacket4_34E or N8AVPacket3_23E +fix.py will replace the corresponding class found by the one defined in config: +- N8AVPacket4_34E -> N8AVPacket4_30E +''' + +import os +import re +from functools import partial + +import yaml + +if __name__ == '__main__': + + from optparse import OptionParser + + usage = '%prog -s av008.py -d av008_fix.py -c config/libav08.yml' + parser = OptionParser(usage=usage) + + parser.add_option('-s', '--src', help='python source') + parser.add_option('-d', '--dst', help='python destination') + parser.add_option('-c', '--cfg', help='config file') + + (options, args) = parser.parse_args() + + # find dynamic class in config file + # and build a dict: + # Option: N8AVOption4_30E + with open(options.cfg, 'r') as cfp: + y = yaml.load(cfp.read()) + + dcMap = { + 'Option': None, + 'Stream': None, + 'Packet': None, + } + + for cls in y['class']: + rgx = re.search('^(N8AV(%s))' % '|'.join(dcMap), cls) + if rgx: + dcMap[rgx.group(2)] = cls + + fixCount = 0 + print('writing %s...' % options.dst) + with open(options.src, 'r') as sfp: + with open(options.dst, 'w') as dfp: + for line in sfp: + + # fonction call by re.sub + def replClass(dcMap, count, matchobj): + global fixCount + fixCount += 1 + return dcMap[matchobj.group(1)] + + # brute force! + nline = re.sub('N8AV(%s)\dDOT_\d+E' % '|'.join(dcMap), + partial(replClass, dcMap, [fixCount]), + line) + + dfp.write(nline) + + print('%d line(s) fixed!' % fixCount) + diff --git a/tools/gen.py b/tools/gen.py index 7d3ec98..4ca41a1 100755 --- a/tools/gen.py +++ b/tools/gen.py @@ -42,7 +42,7 @@ def main(options): buildDir = os.path.abspath(os.path.join(options.buildDir, '%s_%s' % (options.lib, options.version))) if not os.path.isdir(buildDir): - print('Could not find %s build @ %s' % (options.lib, buildDir)) + print(('Could not find %s build @ %s' % (options.lib, buildDir))) sys.exit(1) pyLibVersion = ''.join(options.version.split('.')[:2]) @@ -65,7 +65,7 @@ def main(options): if os.path.isfile(os.path.join(buildDir, 'include', i)): addInclude.append(i) else: - print('skipping %s' % i) + print(('skipping %s' % i)) addInclude = ' '.join(addInclude) print('generating xml...') @@ -108,11 +108,11 @@ def main(options): xml2pyCmd += preloads run(xml2pyCmd) - print(buildDir + 'lib%s' % os.sep) + print((buildDir + 'lib%s' % os.sep)) stripLibPath(pyFileTmp, pyFile, os.path.join(buildDir, 'lib%s' % os.sep)) print('generation done!') - print('python module: %s' % pyFile) + print(('python module: %s' % pyFile)) if __name__ == '__main__': diff --git a/tools/gen.py.bak b/tools/gen.py.bak new file mode 100755 index 0000000..7d3ec98 --- /dev/null +++ b/tools/gen.py.bak @@ -0,0 +1,137 @@ +#!/usr/bin/env python + +''' +Easily generate python binding for libav/ffmpeg + +require: h2xml & xml2py utilities (see docs/Dev.txt) + +howto: +''' + +import sys +import os +import subprocess +import re + + +def stripLibPath(src, dst, folder): + + ''' Replace full lib path with relative path + ''' + + with open(src, 'r') as fs: + with open(dst, 'w') as fd: + for line in fs: + fd.write(re.sub(folder, '', line)) + + +def run(cmd): + + ''' print and run shell command + ''' + + print(cmd) + + retCode = subprocess.call(cmd, shell=True) + if retCode != 0: + raise RuntimeError('cmd %s failed!' % cmd) + + +def main(options): + + buildDir = os.path.abspath(os.path.join(options.buildDir, '%s_%s' % (options.lib, options.version))) + + if not os.path.isdir(buildDir): + print('Could not find %s build @ %s' % (options.lib, buildDir)) + sys.exit(1) + + pyLibVersion = ''.join(options.version.split('.')[:2]) + pyLibVersion = pyLibVersion.zfill(3) + pyLibPrefix = 'ff' if options.lib == 'ffmpeg' else 'av' + xmlFile = '%s%s.xml' % (pyLibPrefix, pyLibVersion) + pyFileTmp = '%s%s_tmp.py' % (pyLibPrefix, pyLibVersion) + pyFile = '%s%s.py' % (pyLibPrefix, pyLibVersion) + + # include file if exists + _addInclude = [ + 'libavutil/channel_layout.h', # libav >= 9 + 'libavresample/avresample.h', # libav >= 9 + 'libswresample/swresample.h', # ffmpeg >= 1.2 + 'libavutil/frame.h', # libav >= 10 + ] + addInclude = [] + + for i in _addInclude: + if os.path.isfile(os.path.join(buildDir, 'include', i)): + addInclude.append(i) + else: + print('skipping %s' % i) + addInclude = ' '.join(addInclude) + + print('generating xml...') + xmlCmd = 'h2xml -I {0}/include -c libavcodec/avcodec.h'\ + ' libavdevice/avdevice.h libavformat/avformat.h libavutil/avutil.h'\ + ' libavutil/mathematics.h libavutil/rational.h libswscale/swscale.h'\ + ' libavutil/pixdesc.h libavutil/opt.h'\ + ' {2} -o {1}'\ + ' -D__STDC_CONSTANT_MACROS'.format(buildDir, xmlFile, addInclude) + run(xmlCmd) + + # preload libavutil & resample lib (libavresample or swresample) + # libav has libavresample + # ffmpeg has libswresample + preloads = ' --preload ' + os.path.join(buildDir, 'lib', 'libavutil.so') + + libFlag = '' + libResample = os.path.join(buildDir, 'lib', 'libavresample.so') + if os.path.isfile(libResample): + preloads += (' --preload ' + libResample) + libFlag = '-l ' + libResample + + libResample = os.path.join(buildDir, 'lib', 'libswresample.so') + if os.path.isfile(libResample): + preloads += (' --preload ' + libResample) + libFlag = '-l ' + libResample + + print('generating python module') + + if options.lib == 'ffmpeg': + xml2pyCmd = 'xml2py {0} -o {1} -l {2}/lib/libavutil.so {3} -l {2}/lib/libavcodec.so'\ + ' -l {2}/lib/libavformat.so -l {2}/lib/libswscale.so'\ + ' -l {2}/lib/libavfilter.so'\ + ' -l {2}/lib/libavdevice.so'.format(xmlFile, pyFileTmp, buildDir, libFlag) + else: + xml2pyCmd = 'xml2py {0} -o {1} -l {2}/lib/libavutil.so {3} -l {2}/lib/libavcodec.so'\ + ' -l {2}/lib/libavformat.so -l {2}/lib/libavdevice.so'\ + ' -l {2}/lib/libswscale.so'.format(xmlFile, pyFileTmp, buildDir, libFlag) + + xml2pyCmd += preloads + run(xml2pyCmd) + + print(buildDir + 'lib%s' % os.sep) + stripLibPath(pyFileTmp, pyFile, os.path.join(buildDir, 'lib%s' % os.sep)) + + print('generation done!') + print('python module: %s' % pyFile) + + +if __name__ == '__main__': + + from optparse import OptionParser + + usage = '%prog -l libav -v 0.8.1' + parser = OptionParser(usage=usage) + parser.add_option('-l', '--lib', + help='gen python binding for lib: ffmpeg or libav (default: %default)', + default='libav') + parser.add_option('-v', '--version', + help='gen python binding for libav version (ex: 0.8.1)') + + parser.add_option('-b', '--buildDir', + default='./build', + help='where to find libav or ffmpeg build') + + (options, args) = parser.parse_args() + + main(options) + diff --git a/tools/genClean.py b/tools/genClean.py index 9e90eb3..fbaa3dd 100755 --- a/tools/genClean.py +++ b/tools/genClean.py @@ -56,9 +56,9 @@ def wAssignement(fp, yamlDict, src, key, cb=None): notFound = set(yamlDict[key]) - found if notFound: - print 'Could not find:' + print('Could not find:') for t in notFound: - print '- %s' % t + print('- %s' % t) def wClassFields(fp, yamlDict, src): @@ -87,9 +87,9 @@ def wClassFields(fp, yamlDict, src): notFound = set(yamlDict['class']) - classFound if notFound: - print 'Could not find class(es):' + print('Could not find class(es):') for c in notFound: - print '- %s' % c + print('- %s' % c) def wFunctions(fp, yamlDict, src): @@ -97,7 +97,7 @@ def wFunctions(fp, yamlDict, src): fcts = yamlDict['function'] dupFunctions = set([x for x in fcts if fcts.count(x) > 1]) for f in dupFunctions: - print 'Warning: duplicated functions in config: %s' % f + print('Warning: duplicated functions in config: %s' % f) with open(src, 'r') as f: @@ -116,9 +116,9 @@ def wFunctions(fp, yamlDict, src): notFound = set(yamlDict['function']) - functionFound if notFound: - print 'Could not find function(s):' + print('Could not find function(s):') for f in notFound: - print '- %s' % f + print('- %s' % f) def main(options): @@ -129,24 +129,24 @@ def main(options): y = yaml.load(f.read()) fd = open(options.dst, 'w') - print 'writing:' - print 'header...' + print('writing:') + print('header...') wHeader(fd, y) - print 'type...' + print('type...') wAssignement(fd, y, options.src, 'type') - print 'define...' + print('define...') wAssignement(fd, y, options.src, 'define') - print 'define long...' + print('define long...') wAssignement(fd, y, options.src, 'defineLong', longCb) - print 'class...' + print('class...') wClass(fd, y) - print 'alias...' + print('alias...') wAssignement(fd, y, options.src, 'alias') - print 'class data...' + print('class data...') wClassFields(fd, y, options.src) - print 'functions...' + print('functions...') wFunctions(fd, y, options.src) - print 'done: %s' % options.dst + print('done: %s' % options.dst) if __name__ == '__main__': diff --git a/tools/genClean.py.bak b/tools/genClean.py.bak new file mode 100755 index 0000000..9e90eb3 --- /dev/null +++ b/tools/genClean.py.bak @@ -0,0 +1,165 @@ +#!/usr/bin/env python + +''' +pyav tools/build.py + +Generate clean binding +''' + +import os +import re + +import yaml + +def longCb(line): + + # handle long value and remove L at the end of line + # only python2 support this: superInteger = 123456789L + # remove L to support python3 + return re.sub('(\d+)L', '\g<1>', line) + +def wHeader(fp, yamlDict): + + for l in yamlDict['import']: + if not l: + l = '' + fp.write(l+'\n') + fp.write('\n') + for l in yamlDict['cdll']: + fp.write(l+'\n') + fp.write('\n') + +def wClass(fp, yamlDict): + + for l in yamlDict['class']: + fp.write('class %s(Structure):\n\tpass\n\n' % l) + +def wAssignement(fp, yamlDict, src, key, cb=None): + + with open(src, 'r') as f: + + found = set() + #write_line = False + line = f.readline() + while line: + for t in yamlDict[key]: + if line.startswith('%s =' % t): + + if cb: + fp.write(cb(line)) + else: + fp.write(line) + found.add(t) + break + line = f.readline() + fp.write('\n') + + notFound = set(yamlDict[key]) - found + if notFound: + print 'Could not find:' + for t in notFound: + print '- %s' % t + +def wClassFields(fp, yamlDict, src): + + with open(src, 'r') as f: + + write_line = False + line = f.readline() + classFound = set() + while line: + for c in yamlDict['class']: + if line.startswith( '%s._fields_' % c ): + write_line = True + classFound.add(c) + break + + if line.startswith(']'): + if write_line: + fp.write(line) + write_line = False + + if write_line: + fp.write(line) + + line = f.readline() + fp.write('\n') + + notFound = set(yamlDict['class']) - classFound + if notFound: + print 'Could not find class(es):' + for c in notFound: + print '- %s' % c + + +def wFunctions(fp, yamlDict, src): + + fcts = yamlDict['function'] + dupFunctions = set([x for x in fcts if fcts.count(x) > 1]) + for f in dupFunctions: + print 'Warning: duplicated functions in config: %s' % f + + with open(src, 'r') as f: + + line = f.readline() + functionFound = set() + while line: + for fct in fcts: + if re.search('%s(\ =|\.argtypes|\.restype)' % fct, + line): + fp.write(line) + functionFound.add(fct) + + line = f.readline() + + fp.write('\n') + + notFound = set(yamlDict['function']) - functionFound + if notFound: + print 'Could not find function(s):' + for f in notFound: + print '- %s' % f + + +def main(options): + + y = {} + cfgPath = os.path.abspath(options.cfg) + with open(cfgPath, 'r') as f: + y = yaml.load(f.read()) + + fd = open(options.dst, 'w') + print 'writing:' + print 'header...' + wHeader(fd, y) + print 'type...' + wAssignement(fd, y, options.src, 'type') + print 'define...' + wAssignement(fd, y, options.src, 'define') + print 'define long...' + wAssignement(fd, y, options.src, 'defineLong', longCb) + print 'class...' + wClass(fd, y) + print 'alias...' + wAssignement(fd, y, options.src, 'alias') + print 'class data...' + wClassFields(fd, y, options.src) + print 'functions...' + wFunctions(fd, y, options.src) + print 'done: %s' % options.dst + +if __name__ == '__main__': + + from optparse import OptionParser + + usage = '%prog -s av08.py -d av8.py -c config/libav08.yml' + parser = OptionParser(usage=usage) + + parser.add_option('-s', '--src', help='python source') + parser.add_option('-d', '--dst', help='python destination') + parser.add_option('-c', '--cfg', help='config file') + + (options, args) = parser.parse_args() + + main(options) +