quick fix 2

This commit is contained in:
Bartłomiej Patyk
2025-10-22 19:05:25 +02:00
commit e5e64b6dc8
598 changed files with 300649 additions and 0 deletions

View File

@ -0,0 +1,53 @@
'''
FFPyPlayer library
==================
'''
import sys
import site
import os
from os.path import join
import platform
__all__ = ('dep_bins', )
__version__ = '4.5.3'
version = __version__
# the ffmpeg src git version tested and upto date with,
# and including this commit
_ffmpeg_git = 'c926140558c60786dc577b121df6b3c6b430bd98'
# excludes commits bdf9ed41fe4bdf4e254615b7333ab0feb1977e98,
# 1be3d8a0cb77f8d34c1f39b47bf5328fe10c82d7,
# f1907faab4023517af7d10d746b5684cccc5cfcc, and
# 0995e1f1b31f6e937a1b527407ed3e850f138098 because they require ffmpeg 5.1/5.2
# which is too new as of now
# also skipped all show modes and subtitle display related functionality commits
# TODO:
# * Implement CONFIG_SDL to be able to compile without needing SDL at all.
# * Currently, it only supports text subtitles - bitmap subtitles are ignored.
# Unless one uses a filter to overlay the subtitle.
# * We can not yet visualize audio to video. Provide a filter chain link between
# audio to video filters to acomplish this.
dep_bins = []
'''A list of paths to the binaries used by the library. It can be used during
packaging for including required binaries.
It is read only.
'''
for d in [sys.prefix, site.USER_BASE]:
if d is None:
continue
for lib in ('ffmpeg', 'sdl'):
p = join(d, 'share', 'ffpyplayer', lib, 'bin')
if os.path.isdir(p):
os.environ["PATH"] = p + os.pathsep + os.environ["PATH"]
if hasattr(os, 'add_dll_directory'):
os.add_dll_directory(p)
dep_bins.append(p)
if 'SDL_AUDIODRIVER' not in os.environ and platform.system() == 'Windows':
os.environ['SDL_AUDIODRIVER'] = 'DirectSound'

Binary file not shown.

View File

@ -0,0 +1,44 @@
include 'includes/ffmpeg.pxi'
cdef class SWScale(object):
cdef SwsContext *sws_ctx
cdef bytes dst_pix_fmt
cdef str dst_pix_fmt_s
cdef int dst_h
cdef int dst_w
cdef AVPixelFormat src_pix_fmt
cdef int src_h
cdef int src_w
cdef class Image(object):
cdef AVFrame *frame
cdef list byte_planes
cdef AVPixelFormat pix_fmt
cdef int cython_init(self, AVFrame *frame) nogil except 1
cpdef is_ref(Image self)
cpdef is_key_frame(Image self)
cpdef get_linesizes(Image self, keep_align=*)
cpdef get_size(Image self)
cpdef get_pixel_format(Image self)
cpdef get_buffer_size(Image self, keep_align=*)
cpdef get_required_buffers(Image self)
cpdef to_bytearray(Image self, keep_align=*)
cpdef to_memoryview(Image self, keep_align=*)
cdef class ImageLoader(object):
cdef AVFormatContext *format_ctx
cdef AVCodec *codec
cdef AVCodecContext *codec_ctx
cdef AVPacket pkt
cdef AVFrame *frame
cdef bytes filename
cdef char msg[256]
cdef int eof
cpdef next_frame(self)
cdef inline object eof_frame(self)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,46 @@
include "includes/ffmpeg.pxi"
cdef enum MT_lib:
SDL_MT,
Py_MT
cdef class MTMutex(object):
cdef MT_lib lib
cdef void* mutex
cdef int lock(MTMutex self) nogil except 2
cdef int _lock_py(MTMutex self) nogil except 2
cdef int unlock(MTMutex self) nogil except 2
cdef int _unlock_py(MTMutex self) nogil except 2
cdef class MTCond(object):
cdef MT_lib lib
cdef MTMutex mutex
cdef void *cond
cdef int lock(MTCond self) nogil except 2
cdef int unlock(MTCond self) nogil except 2
cdef int cond_signal(MTCond self) nogil except 2
cdef int _cond_signal_py(MTCond self) nogil except 2
cdef int cond_wait(MTCond self) nogil except 2
cdef int _cond_wait_py(MTCond self) nogil except 2
cdef int cond_wait_timeout(MTCond self, uint32_t val) nogil except 2
cdef int _cond_wait_timeout_py(MTCond self, uint32_t val) nogil except 2
cdef class MTThread(object):
cdef MT_lib lib
cdef void* thread
cdef int create_thread(MTThread self, int_void_func func, const char *thread_name, void *arg) nogil except 2
cdef int wait_thread(MTThread self, int *status) nogil except 2
cdef class MTGenerator(object):
cdef MT_lib mt_src
cdef int delay(MTGenerator self, int delay) nogil except 2
cdef lockmgr_func get_lockmgr(MTGenerator self) nogil
cdef lockmgr_func get_lib_lockmgr(MT_lib lib) nogil

View File

@ -0,0 +1,259 @@
__all__ = ('MTGenerator', )
include "includes/ff_consts.pxi"
include "includes/inline_funcs.pxi"
from cpython.ref cimport PyObject
cdef extern from "Python.h":
void Py_INCREF(PyObject *)
void Py_XINCREF(PyObject *)
void Py_DECREF(PyObject *)
ctypedef int (*int_cls_method)(void *) nogil
import traceback
cdef int sdl_initialized = 0
def initialize_sdl():
'''Initializes sdl. Must be called before anything can be used.
It is automatically called by the modules that use SDL.
'''
global sdl_initialized
if sdl_initialized:
return
if SDL_Init(0):
raise ValueError('Could not initialize SDL - %s' % SDL_GetError())
sdl_initialized = 1
initialize_sdl()
cdef class MTMutex(object):
def __cinit__(MTMutex self, MT_lib lib):
self.lib = lib
self.mutex = NULL
if lib == SDL_MT:
self.mutex = SDL_CreateMutex()
if self.mutex == NULL:
raise Exception('Cannot create mutex.')
elif lib == Py_MT:
import threading
mutex = threading.Lock()
self.mutex = <PyObject *>mutex
Py_INCREF(<PyObject *>self.mutex)
def __dealloc__(MTMutex self):
if self.lib == SDL_MT:
if self.mutex != NULL:
SDL_DestroyMutex(<SDL_mutex *>self.mutex)
elif self.lib == Py_MT:
Py_DECREF(<PyObject *>self.mutex)
cdef int lock(MTMutex self) nogil except 2:
if self.lib == SDL_MT:
return SDL_mutexP(<SDL_mutex *>self.mutex)
elif self.lib == Py_MT:
return self._lock_py()
cdef int _lock_py(MTMutex self) nogil except 2:
with gil:
return not (<object>self.mutex).acquire()
cdef int unlock(MTMutex self) nogil except 2:
if self.lib == SDL_MT:
return SDL_mutexV(<SDL_mutex *>self.mutex)
elif self.lib == Py_MT:
return self._unlock_py()
cdef int _unlock_py(MTMutex self) nogil except 2:
with gil:
(<object>self.mutex).release()
return 0
cdef class MTCond(object):
def __cinit__(MTCond self, MT_lib lib):
self.lib = lib
self.mutex = MTMutex.__new__(MTMutex, lib)
self.cond = NULL
if self.lib == SDL_MT:
self.cond = SDL_CreateCond()
if self.cond == NULL:
raise Exception('Cannot create condition.')
elif self.lib == Py_MT:
import threading
cond = threading.Condition(<object>self.mutex.mutex)
self.cond = <PyObject *>cond
Py_INCREF(<PyObject *>self.cond)
def __dealloc__(MTCond self):
if self.lib == SDL_MT:
if self.cond != NULL:
SDL_DestroyCond(<SDL_cond *>self.cond)
elif self.lib == Py_MT:
Py_DECREF(<PyObject *>self.cond)
cdef int lock(MTCond self) nogil except 2:
self.mutex.lock()
cdef int unlock(MTCond self) nogil except 2:
self.mutex.unlock()
cdef int cond_signal(MTCond self) nogil except 2:
if self.lib == SDL_MT:
return SDL_CondSignal(<SDL_cond *>self.cond)
elif self.lib == Py_MT:
return self._cond_signal_py()
cdef int _cond_signal_py(MTCond self) nogil except 2:
with gil:
(<object>self.cond).notify()
return 0
cdef int cond_wait(MTCond self) nogil except 2:
if self.lib == SDL_MT:
return SDL_CondWait(<SDL_cond *>self.cond, <SDL_mutex *>self.mutex.mutex)
elif self.lib == Py_MT:
return self._cond_wait_py()
cdef int _cond_wait_py(MTCond self) nogil except 2:
with gil:
(<object>self.cond).wait()
return 0
cdef int cond_wait_timeout(MTCond self, uint32_t val) nogil except 2:
if self.lib == SDL_MT:
return SDL_CondWaitTimeout(<SDL_cond *>self.cond, <SDL_mutex *>self.mutex.mutex, val)
elif self.lib == Py_MT:
return self._cond_wait_timeout_py(val)
cdef int _cond_wait_timeout_py(MTCond self, uint32_t val) nogil except 2:
with gil:
(<object>self.cond).wait(val / 1000.)
return 0
def enterance_func(target_func, target_arg):
return (<int_void_func><uintptr_t>target_func)(<void *><uintptr_t>target_arg)
cdef class MTThread(object):
def __cinit__(MTThread self, MT_lib lib):
self.lib = lib
self.thread = NULL
def __dealloc__(MTThread self):
if self.lib == Py_MT and self.thread != NULL:
Py_DECREF(<PyObject *>self.thread)
cdef int create_thread(MTThread self, int_void_func func, const char *thread_name, void *arg) nogil except 2:
if self.lib == SDL_MT:
with gil:
self.thread = SDL_CreateThread(func, thread_name, arg)
if self.thread == NULL:
raise Exception('Cannot create thread.')
elif self.lib == Py_MT:
with gil:
import threading
thread = threading.Thread(group=None, target=enterance_func,
name=None, args=(<uintptr_t>func, <uintptr_t>arg), kwargs={})
self.thread = <PyObject *>thread
Py_INCREF(<PyObject *>self.thread)
thread.start()
return 0
cdef int wait_thread(MTThread self, int *status) nogil except 2:
if self.lib == SDL_MT:
if self.thread != NULL:
SDL_WaitThread(<SDL_Thread *>self.thread, status)
elif self.lib == Py_MT:
with gil:
(<object>self.thread).join()
if status != NULL:
status[0] = 0
return 0
cdef int_cls_method mutex_lock = <int_cls_method>MTMutex.lock
cdef int_cls_method mutex_release = <int_cls_method>MTMutex.unlock
cdef int _SDL_lockmgr_py(void ** mtx, int op) with gil:
cdef bytes msg
cdef int res = 1
cdef MTMutex mutex
try:
if op == FF_LOCK_CREATE:
mutex = MTMutex.__new__(MTMutex, SDL_MT)
Py_INCREF(<PyObject *>mutex)
mtx[0] = <PyObject *>mutex
res = 0
elif op == FF_LOCK_DESTROY:
if mtx[0] != NULL:
Py_DECREF(<PyObject *>mtx[0])
res = 0
except:
msg = traceback.format_exc().encode('utf8')
av_log(NULL, AV_LOG_ERROR, '%s', msg)
return res
cdef int SDL_lockmgr(void ** mtx, int op) nogil:
if op == FF_LOCK_OBTAIN:
return not not mutex_lock(mtx[0])
elif op == FF_LOCK_RELEASE:
return not not mutex_release(mtx[0])
else:
return _SDL_lockmgr_py(mtx, op)
cdef int Py_lockmgr(void ** mtx, int op) with gil:
cdef int res = 1
cdef bytes msg
cdef MTMutex mutex
try:
if op == FF_LOCK_CREATE:
mutex = MTMutex.__new__(MTMutex, Py_MT)
Py_INCREF(<PyObject *>mutex)
mtx[0] = <PyObject *>mutex
res = 0
elif op == FF_LOCK_OBTAIN:
mutex = <MTMutex>mtx[0]
res = not not mutex.lock() # force it to 0, or 1
elif op == FF_LOCK_RELEASE:
mutex = <MTMutex>mtx[0]
res = not not mutex.unlock()
elif op == FF_LOCK_DESTROY:
if mtx[0] != NULL:
Py_DECREF(<PyObject *>mtx[0])
res = 0
except:
msg = traceback.format_exc().encode('utf8')
av_log(NULL, AV_LOG_ERROR, '%s', msg)
res = 1
return res
cdef lockmgr_func get_lib_lockmgr(MT_lib lib) nogil:
if lib == SDL_MT:
return SDL_lockmgr
elif lib == Py_MT:
return Py_lockmgr
cdef class MTGenerator(object):
def __cinit__(MTGenerator self, MT_lib mt_src, **kwargs):
self.mt_src = mt_src
cdef int delay(MTGenerator self, int delay) nogil except 2:
if self.mt_src == SDL_MT:
SDL_Delay(delay)
elif self.mt_src == Py_MT:
with gil:
import time
time.sleep(delay / 1000.)
return 0
cdef lockmgr_func get_lockmgr(MTGenerator self) nogil:
return get_lib_lockmgr(self.mt_src)

Binary file not shown.

View File

@ -0,0 +1,851 @@
'''
FFmpeg tools
============
Module for manipulating and finding information of FFmpeg formats, codecs,
devices, pixel formats and more.
'''
__all__ = (
'initialize_sdl_aud', 'loglevels', 'codecs_enc', 'codecs_dec', 'pix_fmts',
'formats_in', 'formats_out', 'set_log_callback', 'get_log_callback',
'set_loglevel', 'get_loglevel', 'get_codecs', 'get_fmts',
'get_format_codec',
'get_supported_framerates', 'get_supported_pixfmts', 'get_best_pix_fmt',
'emit_library_info',
'list_dshow_devices', 'encode_to_bytes', 'decode_to_unicode',
'convert_to_str', 'list_dshow_opts')
include "includes/ffmpeg.pxi"
include "includes/inline_funcs.pxi"
cdef extern from "stdlib.h" nogil:
void *malloc(size_t)
void free(void *)
from ffpyplayer.threading cimport Py_MT, MTMutex, get_lib_lockmgr, SDL_MT
import ffpyplayer.threading # for sdl init
import re
import sys
from functools import partial
cdef int sdl_aud_initialized = 0
def initialize_sdl_aud():
'''Initializes sdl audio subsystem. Must be called before audio can be used.
It is automatically called by the modules that use SDL audio.
'''
global sdl_aud_initialized
if sdl_aud_initialized:
return
# Try to work around an occasional ALSA buffer underflow issue when the
# period size is NPOT due to ALSA resampling by forcing the buffer size.
if not SDL_getenv("SDL_AUDIO_ALSA_SET_BUFFER_SIZE"):
SDL_setenv("SDL_AUDIO_ALSA_SET_BUFFER_SIZE", "1", 0)
if SDL_InitSubSystem(SDL_INIT_AUDIO):
raise ValueError('Could not initialize SDL audio - %s' % SDL_GetError())
sdl_aud_initialized = 1
cdef int ffmpeg_initialized = 0
def _initialize_ffmpeg():
'''Initializes ffmpeg libraries. Must be called before anything can be used.
Called automatically when importing this module.
'''
global ffmpeg_initialized
if not ffmpeg_initialized:
av_log_set_flags(AV_LOG_SKIP_REPEATED)
IF CONFIG_AVDEVICE:
avdevice_register_all()
avformat_network_init()
ffmpeg_initialized = 1
_initialize_ffmpeg()
def _get_item0(x):
return x[0]
'see http://ffmpeg.org/ffmpeg.html for log levels'
loglevels = {
"quiet": AV_LOG_QUIET, "panic": AV_LOG_PANIC, "fatal": AV_LOG_FATAL,
"error": AV_LOG_ERROR, "warning": AV_LOG_WARNING, "info": AV_LOG_INFO,
"verbose": AV_LOG_VERBOSE, "debug": AV_LOG_DEBUG, "trace": AV_LOG_TRACE}
'''A dictionary with all the available ffmpeg log levels. The keys are the loglevels
and the values are their ffmpeg values. The lower the value, the more important
the log. Note, this is ooposite python where the higher the level the more important
the log.
'''
_loglevel_inverse = {v:k for k, v in loglevels.iteritems()}
cdef object _log_callback = None
cdef MTMutex _log_mutex= MTMutex(SDL_MT)
cdef int log_level = AV_LOG_WARNING
cdef int print_prefix = 1
cdef void gil_call_callback(char *line, int level):
cdef object callback
callback = _log_callback
if callback is None:
return
callback(tcode(line), _loglevel_inverse[level])
cdef void call_callback(char *line, int level) nogil:
with gil:
gil_call_callback(line, level)
cdef void _log_callback_func(void* ptr, int level, const char* fmt, va_list vl) noexcept nogil:
cdef char line[2048]
if fmt == NULL or level > log_level:
return
av_log_format_line(ptr, level, fmt, vl, line, sizeof(line), &print_prefix)
call_callback(line, level)
def _logger_callback(logger_dict, message, level):
message = message.strip()
if message:
logger_dict[level]('FFPyPlayer: {}'.format(message))
def set_log_callback(object callback=None, logger=None, int default_only=False):
'''Sets a callback to be used by ffmpeg when emitting logs.
This function is thread safe.
See also :func:`set_loglevel`.
:Parameters:
`callback`: callable or None
A function which will be called with strings to be printed. It takes
two parameters: ``message`` and ``level``. ``message`` is the string
to be printed. ``level`` is the log level of the string and is one
of the keys of the :attr:`loglevels` dict. If ``callback`` and ``logger``
are None, the default ffmpeg log callback will be set, which prints to stderr.
Defaults to None.
`logger`: a python logger object or None
If ``callback`` is None and this is not None, this logger object's
``critical``, ``error``, ``warning``, ``info``, ``debug``, and ``trace``
methods will be called directly to forward ffmpeg's log outputs.
.. note::
If the logger doesn't have a trace method, the trace output will be
redirected to debug. However, the trace level outputs a lot of logs.
`default_only`: bool
If True, when ``callback`` or ``logger`` are not ``None``, they
will only be set if a callback or logger has not already been set.
:returns:
The previous callback set (None, if it has not been set).
>>> from ffpyplayer.tools import set_log_callback, loglevels
>>> loglevel_emit = 'error' # This and worse errors will be emitted.
>>> def log_callback(message, level):
... message = message.strip()
... if message and loglevels[level] <= loglevels[loglevel_emit]:
... print '%s: %s' %(level, message.strip())
>>> set_log_callback(log_callback)
...
>>> set_log_callback(None)
'''
global _log_callback
if callback is not None and not callable(callback):
raise Exception('Log callback needs to be callable.')
if callback is None and logger is not None:
logger_dict = {
'quiet': logger.critical, 'panic': logger.critical,
'fatal': logger.critical, 'error': logger.error, 'warning': logger.warning,
'info': logger.info, 'verbose': logger.debug, 'debug': logger.debug,
'trace': getattr(logger, 'trace', logger.debug)}
callback = partial(_logger_callback, logger_dict)
_log_mutex.lock()
old_callback = _log_callback
if callback is None:
av_log_set_callback(&av_log_default_callback)
_log_callback = None
elif not default_only or old_callback is None:
av_log_set_callback(&_log_callback_func)
_log_callback = callback
_log_mutex.unlock()
return old_callback
def get_log_callback():
'''Returns the last log callback set, or None if it has not been set.
See :func:`set_log_callback`.
'''
_log_mutex.lock()
old_callback = _log_callback
_log_mutex.unlock()
return old_callback
def set_loglevel(loglevel):
'''This sets the global FFmpeg log level. less important log levels are filtered
and not passsed on to the logger or callback set by :func:`set_log_callback`.
It also set the loglevel of FFmpeg if not callback or logger is set.
The global log level, if not set, defaults to ``'warning'``.
:Parameters:
`loglevel`: str
The log level. Can be one of the keys of :attr:`loglevels`.
'''
cdef int level
global log_level
if loglevel not in loglevels:
raise ValueError('Invalid loglevel {}'.format(loglevel))
level = loglevels[loglevel]
_log_mutex.lock()
av_log_set_level(level)
log_level = level
_log_mutex.unlock()
set_loglevel(_loglevel_inverse[log_level])
def get_loglevel():
'''Returns the log level set with :func:`set_loglevel`, or the default level if not
set. It is one of the keys of :attr:`loglevels`.
'''
cdef int level
_log_mutex.lock()
level = log_level
_log_mutex.unlock()
return _loglevel_inverse[level]
cpdef get_codecs(
int encode=False, int decode=False, int video=False, int audio=False,
int data=False, int subtitle=False, int attachment=False, other=False):
'''Returns a list of codecs (e.g. h264) that is available by ffpyplayer for
encoding or decoding and matches the media types, e.g. video or audio.
The parameters determine which codecs is included in the result. The parameters
all default to False.
:Parameters:
`encode`: bool
If True, includes the encoding codecs in the result. Defaults to False.
`decode`: bool
If True, includes the decoding codecs in the result. Defaults to False.
`video`: bool
If True, includes the video codecs in the result. Defaults to False.
`audio`: bool
If True, includes the audio codecs in the result. Defaults to False.
`data`: bool
If True, includes the (continuous) side data codecs in the result. Defaults to False.
`subtitle`: bool
If True, includes the subtitle codecs in the result. Defaults to False.
`attachment`: bool
If True, includes the (sparse) data attachment codecs in the result. Defaults to False.
`other`: bool
If True, returns all the codec media types.
:returns:
A sorted list of the matching codec names.
'''
cdef list codecs = []
cdef AVCodec *codec = NULL
cdef void *iter_codec = NULL
codec = av_codec_iterate(&iter_codec)
while codec != NULL:
if ((encode and av_codec_is_encoder(codec) or
decode and av_codec_is_decoder(codec)) and
(video and codec.type == AVMEDIA_TYPE_VIDEO or
audio and codec.type == AVMEDIA_TYPE_AUDIO or
data and codec.type == AVMEDIA_TYPE_DATA or
subtitle and codec.type == AVMEDIA_TYPE_SUBTITLE or
attachment and codec.type == AVMEDIA_TYPE_ATTACHMENT or
other)):
codecs.append(tcode(codec.name))
codec = av_codec_iterate(&iter_codec)
return sorted(codecs)
codecs_enc = get_codecs(encode=True, video=True)
'''A list of all the codecs available for encoding video. '''
codecs_dec = get_codecs(decode=True, video=True, audio=True)
'''A list of all the codecs available for decoding video and audio. '''
cdef list list_pixfmts():
cdef list fmts = []
cdef const AVPixFmtDescriptor *desc = NULL
desc = av_pix_fmt_desc_next(desc)
while desc != NULL:
fmts.append(tcode(desc.name))
desc = av_pix_fmt_desc_next(desc)
return sorted(fmts)
pix_fmts = list_pixfmts()
'''A list of all the pixel formats available to ffmpeg. '''
cpdef get_fmts(int input=False, int output=False):
'''Returns the formats available in FFmpeg.
:Parameters:
`input`: bool
If True, also includes input formats in the result. Defaults to False
`output`: bool
If True, also includes output formats in the result. Defaults to False
:returns:
A 3-tuple of 3 lists, ``formats``, ``full_names``, and ``extensions``.
Each of the three lists are of identical length.
`formats`: list
A list of the names of the formats.
`full_names`: list
A list of the corresponding human readable names for each of the
formats. Can be the empty string if none is available.
`extensions`: list
A list of the extensions associated with the corresponding formats.
Each item is a (possibly empty) list of extensions names.
'''
cdef list fmts = [], full_names = [], exts = []
cdef AVOutputFormat *ofmt = NULL
cdef AVInputFormat *ifmt = NULL
cdef void *ifmt_opaque = NULL
cdef void *ofmt_opaque = NULL
cdef object names, full_name, ext
if output:
ofmt = av_muxer_iterate(&ofmt_opaque)
while ofmt != NULL:
if ofmt.name != NULL:
names = tcode(ofmt.name).split(',')
full_name = tcode(ofmt.long_name) if ofmt.long_name != NULL else ''
ext = tcode(ofmt.extensions).split(',') if ofmt.extensions != NULL else []
fmts.extend(names)
full_names.extend([full_name, ] * len(names))
exts.extend([ext, ] * len(names))
ofmt = av_muxer_iterate(&ofmt_opaque)
if input:
ifmt = av_demuxer_iterate(&ifmt_opaque)
while ifmt != NULL:
if ifmt.name != NULL:
names = tcode(ifmt.name).split(',')
full_name = tcode(ifmt.long_name) if ifmt.long_name != NULL else ''
ext = tcode(ifmt.extensions).split(',') if ifmt.extensions != NULL else []
fmts.extend(names)
full_names.extend([full_name, ] * len(names))
exts.extend([ext, ] * len(names))
ifmt = av_demuxer_iterate(&ifmt_opaque)
exts = [x for (y, x) in sorted(zip(fmts, exts), key=_get_item0)]
full_names = [x for (y, x) in sorted(zip(fmts, full_names), key=_get_item0)]
fmts = sorted(fmts)
return fmts, full_names, exts
formats_in = get_fmts(input=True)[0]
'''A list of all the formats (e.g. file formats) available for reading. '''
formats_out = get_fmts(output=True)[0]
'''A list of all the formats (e.g. file formats) available for writing. '''
def get_format_codec(filename=None, fmt=None):
'''Returns the best codec associated with the file format. The format
can be provided using either ``filename`` or ``fmt``.
:Parameters:
`filename`: str or None
The output filename. If provided, the extension of the filename
is used to guess the format.
`fmt`: str or None.
The format to use. Can be one of :attr:`ffpyplayer.tools.formats_out`.
:returns:
str:
The name from :attr:`ffpyplayer.tools.codecs_enc`
of the best codec that can be used with this format.
For example:
.. code-block:: python
>>> get_format_codecs('test.png')
'mjpeg'
>>> get_format_codecs('test.jpg')
'mjpeg'
>>> get_format_codecs('test.mkv')
'libx264'
>>> get_format_codecs(fmt='h264')
'libx264'
'''
cdef int res
cdef char *format_name = NULL
cdef char *name = NULL
cdef const AVCodec *codec_desc = NULL
cdef AVFormatContext *fmt_ctx = NULL
cdef char msg[256]
cdef AVCodecID codec_id
if fmt:
fmt = fmt.encode('utf8')
format_name = fmt
if filename:
filename = filename.encode('utf8')
name = filename
res = avformat_alloc_output_context2(&fmt_ctx, NULL, format_name, name)
if res < 0 or fmt_ctx == NULL or fmt_ctx.oformat == NULL:
raise Exception('Failed to find format: ' + tcode(emsg(res, msg, sizeof(msg))))
codec_id = fmt_ctx.oformat.video_codec
codec_desc = avcodec_find_encoder(codec_id)
if codec_desc == NULL:
raise Exception('Default codec not found for format')
return tcode(codec_desc.name)
def get_supported_framerates(codec_name, rate=()):
'''Returns the supported frame rates for encoding codecs. If a desired rate is
provided, it also returns the closest valid rate.
:Parameters:
`codec_name`: str
The name of a encoding codec.
`rate`: 2-tuple of ints, or empty tuple.
If provided, a 2-tuple where the first element is the numerator,
and the second the denominator of the frame rate we wish to use. E.g.
(2997, 100) means a frame rate of 29.97.
:returns:
(list of 2-tuples, or empty list):
If there are no restrictions on the frame rate (i.e. all rates are valid)
it returns a empty list, otherwise it returns a list with the valid
frame rates. If `rate` is provided and there are restrictions on the frame
rates, the closest frame rate is the zero'th element in the list.
For example:
.. code-block:: python
>>> print get_supported_framerates('mpeg1video')
[(24000, 1001), (24, 1), (25, 1), (30000, 1001), (30, 1), (50, 1),
(60000, 1001), (60, 1), (15, 1), (5, 1), (10, 1), (12, 1), (15, 1)]
>>> print get_supported_framerates('mpeg1video', (2997, 100))
[(30000, 1001), (24000, 1001), (24, 1), (25, 1), (30, 1), (50, 1),
(60000, 1001), (60, 1), (15, 1), (5, 1), (10, 1), (12, 1), (15, 1)]
'''
cdef AVRational rate_struct
cdef list rate_list = []
cdef int i = 0
cdef bytes name = codec_name if isinstance(codec_name, bytes) else codec_name.encode('utf8')
cdef AVCodec *codec = avcodec_find_encoder_by_name(name)
if codec == NULL:
raise Exception('Encoder codec %s not available.' % codec_name)
if codec.supported_framerates == NULL:
return rate_list
while codec.supported_framerates[i].den:
rate_list.append((codec.supported_framerates[i].num, codec.supported_framerates[i].den))
i += 1
if rate:
rate_struct.num, rate_struct.den = rate
i = av_find_nearest_q_idx(rate_struct, codec.supported_framerates)
rate = rate_list[i]
del rate_list[i]
rate_list.insert(0, rate)
return rate_list
def get_supported_pixfmts(codec_name, pix_fmt=''):
'''Returns the supported pixel formats for encoding codecs. If a desired format
is provided, it also returns the closest format (i.e. the format with minimum
conversion loss).
:Parameters:
`codec_name`: str
The name of a encoding codec.
`pix_fmt`: str
If not empty, the name of a pixel format we wish to use with this codec,
e.g. 'rgb24'.
:returns:
(list of pixel formats, or empty list):
If there are no restrictions on the pixel formats (i.e. all the formats
are valid) it returns a empty list, otherwise it returns a list with the
valid formats. If pix_fmt is not empty and there are restrictions to the
formats, the closest format which results in the minimum loss when converting
will be returned as the zero'th element in the list.
For example:
.. code-block:: python
>>> print get_supported_pixfmts('ffv1')
['yuv420p', 'yuva420p', 'yuva422p', 'yuv444p', 'yuva444p', 'yuv440p', ...
'gray16le', 'gray', 'gbrp9le', 'gbrp10le', 'gbrp12le', 'gbrp14le']
>>> print get_supported_pixfmts('ffv1', 'gray')
['gray', 'yuv420p', 'yuva420p', 'yuva422p', 'yuv444p', 'yuva444p', ...
'gray16le', 'gbrp9le', 'gbrp10le', 'gbrp12le', 'gbrp14le']
'''
cdef AVPixelFormat fmt
cdef bytes name = codec_name if isinstance(codec_name, bytes) else codec_name.encode('utf8')
cdef bytes fmt_b = pix_fmt if isinstance(pix_fmt, bytes) else pix_fmt.encode('utf8')
cdef list fmt_list = []
cdef int i = 0, loss = 0, has_alpha = 0
cdef AVCodec *codec = avcodec_find_encoder_by_name(name)
if codec == NULL:
raise Exception('Encoder codec %s not available.' % codec_name)
if pix_fmt and av_get_pix_fmt(fmt_b) == AV_PIX_FMT_NONE:
raise Exception('Pixel format not recognized.')
if codec.pix_fmts == NULL:
return fmt_list
while codec.pix_fmts[i] != AV_PIX_FMT_NONE:
fmt_list.append(tcode(av_get_pix_fmt_name(codec.pix_fmts[i])))
i += 1
if pix_fmt:
# XXX: fix this to check if NULL (although kinda already checked above)
has_alpha = av_pix_fmt_desc_get(av_get_pix_fmt(fmt_b)).nb_components % 2 == 0
fmt = avcodec_find_best_pix_fmt_of_list(codec.pix_fmts, av_get_pix_fmt(fmt_b),
has_alpha, &loss)
i = fmt_list.index(tcode(av_get_pix_fmt_name(fmt)))
pix = fmt_list[i]
del fmt_list[i]
fmt_list.insert(0, pix)
return fmt_list
def get_best_pix_fmt(pix_fmt, pix_fmts):
'''Returns the best pixel format with the least conversion loss from the
original pixel format, given a list of potential pixel formats.
:Parameters:
`pix_fmt`: str
The name of a original pixel format.
`pix_fmts`: list-type of strings
A list of possible pixel formats from which the best will be chosen.
:returns:
The pixel format with the least conversion loss.
.. note::
The returned pixel format seems to be somewhat sensitive to the order
of the input pixel formats. Higher quality pixel formats should therefore
be at the beginning of the list.
For example:
.. code-block:: python
>>> get_best_pix_fmt('yuv420p', ['rgb24', 'rgba', 'yuv444p', 'gray'])
'rgb24'
>>> get_best_pix_fmt('gray', ['rgb24', 'rgba', 'yuv444p', 'gray'])
'gray'
>>> get_best_pix_fmt('rgb8', ['rgb24', 'yuv420p', 'rgba', 'yuv444p', 'gray'])
'rgb24'
'''
cdef AVPixelFormat fmt, fmt_src
cdef bytes fmt_src_b = pix_fmt if isinstance(pix_fmt, bytes) else pix_fmt.encode('utf8')
cdef bytes fmt_b
cdef int i = 0, loss = 0, has_alpha = 0
cdef AVPixelFormat *fmts = NULL
if not pix_fmt or not pix_fmts:
raise ValueError('Invalid arguments {}, {}'.format(pix_fmt, pix_fmts))
fmt_src = av_get_pix_fmt(fmt_src_b)
if fmt_src == AV_PIX_FMT_NONE:
raise Exception('Pixel format {} not recognized.'.format(pix_fmt))
fmts = <AVPixelFormat *>malloc(sizeof(AVPixelFormat) * (len(pix_fmts) + 1))
if fmts == NULL:
raise MemoryError()
try:
fmts[len(pix_fmts)] = AV_PIX_FMT_NONE
for i, fmt_s in enumerate(pix_fmts):
fmt_b = fmt_s if isinstance(fmt_s, bytes) else fmt_s.encode('utf8')
fmts[i] = av_get_pix_fmt(fmt_b)
if fmts[i] == AV_PIX_FMT_NONE:
raise Exception('Pixel format {} not recognized.'.format(fmt_s))
has_alpha = av_pix_fmt_desc_get(fmt_src).nb_components % 2 == 0
fmt = avcodec_find_best_pix_fmt_of_list(fmts, fmt_src, has_alpha, &loss)
finally:
free(fmts)
return tcode(av_get_pix_fmt_name(fmt))
def emit_library_info():
'''Prints to the ffmpeg log all the ffmpeg library's versions and configure
options.
'''
print_all_libs_info(INDENT|SHOW_CONFIG, AV_LOG_INFO)
print_all_libs_info(INDENT|SHOW_VERSION, AV_LOG_INFO)
def _dshow_log_callback(log, message, level):
message = message.encode('utf8')
if not log:
log.append((message, level))
return
last_msg, last_level = log[-1]
if last_level == level:
log[-1] = last_msg + message, level
else:
log.append((message, level))
cpdef int list_dshow_opts(list log, bytes stream, bytes option) except 1:
cdef AVFormatContext *fmt = NULL
cdef AVDictionary* opts = NULL
cdef AVInputFormat *ifmt
cdef object old_callback
cdef int level
cdef list temp_log = []
cdef bytes item
global log_level
ifmt = av_find_input_format(b"dshow")
if ifmt == NULL:
raise Exception('Direct show not found.')
av_dict_set(&opts, option, b"true", 0)
_log_mutex.lock()
old_callback = set_log_callback(partial(_dshow_log_callback, temp_log))
level = log_level
av_log_set_level(AV_LOG_TRACE)
log_level = AV_LOG_TRACE
avformat_open_input(&fmt, stream, ifmt, &opts)
av_log_set_level(level)
log_level = level
set_log_callback(old_callback)
_log_mutex.unlock()
avformat_close_input(&fmt)
av_dict_free(&opts)
for item, l in temp_log:
for line in item.splitlines():
log.append((line, l))
return 0
def list_dshow_devices():
'''Returns a list of the dshow devices available.
:returns:
`3-tuple`: A 3-tuple, of (`video`, `audio`, `names`)
`video`: dict
A dict of all the direct show **video** devices. The keys
of the dict are the unique names of the available direct show devices. The values
are a list of the available configurations for that device. Each
element in the list has the following format:
``(pix_fmt, codec_fmt, (frame_width, frame_height), (min_framerate, max_framerate))``
`audio`: dict
A dict of all the direct show **audio** devices. The keys
of the dict are the unique names of the available direct show devices. The values
are a list of the available configurations for that device. Each
element in the list has the following format:
``((min_num_channels, min_num_channels), (min_bits, max_bits), (min_rate, max_rate))``.
`names`: dict
A dict mapping the unique names of the video and audio devices to
a more human friendly (possibly non-unique) name. Either of these
names can be used when opening the device. However, if using the non-unique
name, it's not guarenteed which of the devices sharing the name will be opened.
For example:
.. code-block:: python
>>> from ffpyplayer.player import MediaPlayer
>>> from ffpyplayer.tools import list_dshow_devices
>>> import time, weakref
>>> dev = list_dshow_devices()
>>> print dev
({'@device_pnp_...223196\\global': [('bgr24', '', (160, 120), (5, 30)),
('bgr24', '', (176, 144), (5, 30)), ('bgr24', '', (320, 176), (5, 30)),
('bgr24', '', (320, 240), (5, 30)), ('bgr24', '', (352, 288), (5, 30)),
...
('yuv420p', '', (320, 240), (5, 30)), ('yuv420p', '', (352, 288), (5, 30))],
'@device_pnp_...223196\\global': [('bgr24', '', (160, 120), (30, 30)),
...
('yuyv422', '', (352, 288), (30, 30)),
('yuyv422', '', (640, 480), (30, 30))]},
{'@device_cm_...2- HD Webcam C615)': [((1, 2), (8, 16), (11025, 44100))],
'@device_cm_...HD Webcam C615)': [((1, 2), (8, 16), (11025, 44100))]},
{'@device_cm_...- HD Webcam C615)': 'Microphone (2- HD Webcam C615)',
'@device_cm_...2- HD Webcam C615)': 'Microphone (3- HD Webcam C615)',
...
'@device_pnp...223196\\global': 'HD Webcam C615',
'@device_pnp...223196\\global': 'Laptop Integrated Webcam'})
See :ref:`dshow-example` for a full example.
'''
cdef list res = []
cdef dict video = {}, audio = {}, curr = None
cdef object last
cdef bytes msg, msg2
cdef dict name_map = {}
# list devices
list_dshow_opts(res, b'dummy', b'list_devices')
# primary dev name
pname = re.compile(' *\[dshow *@ *[\w]+\] *"(.+)" *\\((video|audio)\\) *')
# alternate dev name
apname = re.compile(' *\[dshow *@ *[\w]+\] *Alternative name *"(.+)" *')
m = None
for msg, level in res:
message = msg.decode('utf8')
# do we match a primary name - i.e. next device
m_temp = pname.match(message)
if m_temp:
m = m_temp
curr = audio if m.group(2) == 'audio' else video
curr[m.group(1)] = []
name_map[m.group(1)] = m.group(1)
continue
m_temp = apname.match(message)
# if we match alternate name and already have primary, then we're adding it
if m_temp and m:
curr[m_temp.group(1)] = []
name_map[m_temp.group(1)] = m.group(1)
del curr[m.group(1)]
del name_map[m.group(1)]
else:
msg2 = message.encode('utf8')
av_log(NULL, loglevels[level], '%s', msg2)
m = None
# list video devices options
vid_opts = re.compile(' *\[dshow *@ *[\w]+\] +(pixel_format|vcodec)=([\w]+) +min +s=\d+x\d+ +fps=(\d+)\
+max +s=(\d+)x(\d+) +fps=(\d+).*')
pheader1 = re.compile(' *\[dshow *@ *[\w]+\] *(?:Pin|Selecting pin) (?:"Capture"|"Output"|Capture|Output).*')
pheader2 = re.compile(' *\[dshow *@ *[\w]+\] *DirectShow (?:video|audio) (?:only )?device options.*')
for video_stream in video:
res = []
list_dshow_opts(res, ("video=%s" % video_stream).encode('utf8'), b'list_options')
for msg, level in res:
message = msg.decode('utf8')
opts = vid_opts.match(message)
if not opts:
if not pheader1.match(message) and not pheader2.match(message):
av_log(NULL, loglevels[level], '%s', msg)
continue
g1, g2, g3, g4, g5, g6 = opts.groups()
if g1 == 'pixel_format':
item = g2, "", (int(g4), int(g5)), (int(g3), int(g6))
else:
item = "", g2, (int(g4), int(g5)), (int(g3), int(g6))
if item not in video[video_stream]:
video[video_stream].append(item)
video[video_stream] = sorted(video[video_stream])
# list audio devices options
paud_opts = re.compile(' *\[dshow *@ *[\w]+\] +ch= *(\d+), +bits= *(\d+),\
+rate= *(\d+).*')
for audio_stream in audio:
res = []
list_dshow_opts(res, ("audio=%s" % audio_stream).encode('utf8'), b'list_options')
for msg, level in res:
message = msg.decode('utf8')
mopts = paud_opts.match(message)
if mopts:
opts = (int(mopts.group(1)), int(mopts.group(2)), int(mopts.group(3)))
if opts not in audio[audio_stream]:
audio[audio_stream].append(opts)
elif (not pheader1.match(message)) and (not pheader2.match(message)):
av_log(NULL, loglevels[level], '%s', msg)
audio[audio_stream] = sorted(audio[audio_stream])
return video, audio, name_map
cdef object encode_text(object item, int encode):
if isinstance(item, basestring):
if encode:
return item.encode('utf8')
return item.decode('utf8')
if isinstance(item, dict):
for k, v in item.items():
item[k] = encode_text(v, encode)
return item
try:
iter(item)
except TypeError:
return item
return item.__class__((encode_text(i, encode) for i in item))
def encode_to_bytes(item):
'''Takes the item and walks it recursively whether it's a string, int, iterable,
etc. and encodes all the strings to utf-8.
:Parameters:
`item`: anything
The object to be walked and encoded.
:returns:
An object identical to the ``item``, but with all strings encoded to utf-8.
'''
return encode_text(item, 1)
def decode_to_unicode(item):
'''Takes the item and walks it recursively whether it's a string, int, iterable,
etc. and encodes all the strings to utf-8.
:Parameters:
`item`: anything
The object to be walked and encoded.
:returns:
An object identical to the ``item``, but with all strings encoded to utf-8.
'''
return encode_text(item, 0)
def convert_to_str(item):
'''Takes the item and walks it recursively whether it's a string, int, iterable,
etc. and encodes all the strings to utf-8.
:Parameters:
`item`: anything
The object to be walked and encoded.
:returns:
An object identical to the ``item``, but with all strings encoded to utf-8.
'''
return encode_text(item, False)

View File

@ -0,0 +1,49 @@
include 'includes/ffmpeg.pxi'
cdef class MediaWriter(object):
cdef AVFormatContext *fmt_ctx
cdef MediaStream *streams
cdef int n_streams
cdef list config
cdef AVDictionary *format_opts
cdef int64_t total_size
cdef int closed
cpdef close(self)
cdef void clean_up(MediaWriter self) nogil
cdef struct MediaStream:
# pointer to the stream to which we're adding frames.
AVStream *av_stream
int index
AVCodec *codec
AVCodecContext *codec_ctx
# codec used to encode video
AVCodecID codec_id
# the size of the frame passed in
int width_in
int width_out
# the size of the frame actually written to disk
int height_in
int height_out
# The denominator of the frame rate of the stream
int den
# The numerator of the frame rate of the stream
int num
# the pixel format of the frame passed in
AVPixelFormat pix_fmt_in
# the pixel format of the frame actually written to disk
# if it's -1 (AV_PIX_FMT_NONE) then input will be used. '''
AVPixelFormat pix_fmt_out
# The frame in which the final image to be written to disk is held, when we
# need to convert.
AVFrame *av_frame
SwsContext *sws_ctx
int count
int64_t pts
int sync_fmt
AVDictionary *codec_opts

View File

@ -0,0 +1,582 @@
'''
FFmpeg based media writer
=========================
A FFmpeg based python media writer. See :class:`MediaWriter` for details.
Currently writes only video.
'''
__all__ = ('MediaWriter', )
include "includes/inline_funcs.pxi"
cdef extern from "string.h" nogil:
void *memset(void *, int, size_t)
cdef extern from "stdlib.h" nogil:
void *malloc(size_t)
void free(void *)
cdef extern from "math.h" nogil:
double floor(double)
cdef extern from "errno.h" nogil:
int ENOENT
int EAGAIN
from ffpyplayer.pic cimport Image
import ffpyplayer.tools # required to init ffmpeg
from ffpyplayer.tools import encode_to_bytes, convert_to_str
from copy import deepcopy
from ffpyplayer.tools import get_supported_framerates, get_supported_pixfmts
DEF VSYNC_PASSTHROUGH = 0
DEF VSYNC_CFR = 1
DEF VSYNC_VFR = 2
DEF VSYNC_DROP = 0xff
cdef int AV_ENOENT = ENOENT if ENOENT < 0 else -ENOENT
cdef int AV_EAGAIN = EAGAIN if EAGAIN < 0 else -EAGAIN
cdef class MediaWriter(object):
'''An FFmpeg based media writer class. Currently only supports video.
With this class one can write images frames stored in many different pixel
formats into a multi-stream video file using :meth:`write_frame`. All FFmpeg
codecs and pixel formats are supported.
:Parameters:
`filename`: str
The filename of the media file to create. Will be encoded using utf8
berfore passing to FFmpeg.
`streams`: list of dicts
A list of streams to create in the file. ``streams``
is a list of dicts, where each dict configures the corresponding stream.
The keywords listed below are available. One can also specify default
values for the keywords for all streams using ``kwargs``. Keywords also
found in ``streams`` will overwrite those in ``kwargs``:
`pix_fmt_in`: str
The pixel format of the :class:`~ffpyplayer.pic.Image`
to be passed to :meth:`write_frame` for this stream. Can be one of
:attr:`ffpyplayer.tools.pix_fmts`.
`width_in`: int
The width of the :class:`ffpyplayer.pic.Image` that will be
passed to :meth:`write_frame` for this stream.
`height_in`: int
The height of the :class:`ffpyplayer.pic.Image` that will be
passed to :meth:`write_frame` for this stream.
`pix_fmt_out`: str
The pixel format in which frames will be
written to the file for this stream. Can be one of
:attr:`ffpyplayer.tools.pix_fmts`. Defaults to ``pix_fmt_in``
if not provided. Not every pixel format is supported for each
encoding codec, see :func:`~ffpyplayer.tools.get_supported_pixfmts`
for which pixel formats are supported for ``codec``.
`width_out`: int
The width at which frames will be written to the file for this
stream. Defaults to ``width_in`` if not provided.
`height_out`: int
The height at which frames will be written to the file for this
stream. Defaults to ``height_in`` if not provided.
`codec`: str
The codec used to write the frames to the file. Can be one of
the encoding codecs in :attr:`ffpyplayer.tools.codecs_enc`.
If not provided, it defaults to the default best codec for the format
provided in ``fmt`` or guessed from the ``filename``.
See :func:`ffpyplayer.tools.get_format_codecs`
`frame_rate`: 2-tuple of ints
A 2-tuple of ints representing the frame rate to be used when writing
the file. The first element is the numerator, while the second is the
denuminator of a ratio describing the rate. E.g. (2997, 100) describes
29.97 fps.
The timestamps of the frames written using :meth:`write_frame` do
not necessarily need to be multiples of the frame rate because they might be
forced to matching timestamps if required. Not every frame rate is
supported for each encoding codec, see
:func:`ffpyplayer.tools.get_supported_framerates` for which frame
rates are supported for ``codec``.
`fmt`: str
The format to use for the output. Can be one of
:attr:`ffpyplayer.tools.formats_out`. Defaults to empty string.
If not provided, ``filename`` will be used determine the format,
otherwise this arg will be used.
`lib_opts`: dict or list of dicts
A dictionary of options that will be passed
to the ffmpeg libraries, codecs, sws, and formats when opening them.
This accepts most of the options that can be passed to ffmpeg libraries.
See below for examples. Both the keywords and values must be strings.
It can be passed a dict in which case it'll be applied to all the streams
or a list containing a dict for each stream.
`metadata`: dict or list of dicts
Metadata that will be written to the streams, if
supported by the stream. See below for examples. Both the keywords and
values must be strings. It can be passed a dict in which case it'll be
applied to all the streams or a list containing a dict for each stream.
If (these) metadata is not supported, it will silently fail to write them.
`overwrite`: bool
Whether we should overwrite an existing file.
If False, an error will be raised if the file already exists. If True,
the file will be overwritten if it exists.
`**kwargs`:
Accepts default values for all ``streams`` which will be used if these
keywords are not provided for any stream.
See :ref:`write-simple` and :ref:`write-h264` for examples.
'''
def __cinit__(self, filename, streams, fmt='', lib_opts={}, metadata={},
overwrite=False, **kwargs):
cdef int res = 0, n = len(streams), r
cdef char *format_name = NULL
cdef char msg[256]
cdef MediaStream *s
cdef AVDictionaryEntry *dict_temp = NULL
cdef bytes msg2
cdef const AVCodec *codec_desc
filename = encode_to_bytes(filename)
streams = encode_to_bytes(deepcopy(streams))
if fmt:
fmt = fmt.encode('utf8')
lib_opts = encode_to_bytes(deepcopy(lib_opts))
metadata = encode_to_bytes(deepcopy(metadata))
kwargs = encode_to_bytes(deepcopy(kwargs))
self.total_size = 0
self.closed = 0
self.format_opts = NULL
if fmt:
format_name = fmt
if not n:
raise Exception('Streams parameters not provided.')
conf = [deepcopy(kwargs) for i in streams]
for r in range(n):
conf[r].update(streams[r])
self.config = conf
self.fmt_ctx = NULL
res = avformat_alloc_output_context2(&self.fmt_ctx, NULL, format_name, filename)
if res < 0 or self.fmt_ctx == NULL:
raise Exception('Failed to create format context: ' + tcode(emsg(res, msg, sizeof(msg))))
self.streams = <MediaStream *>malloc(n * sizeof(MediaStream))
if self.streams == NULL:
self.clean_up()
raise MemoryError()
s = self.streams
self.n_streams = n
memset(s, 0, n * sizeof(MediaStream))
if isinstance(lib_opts, dict):
lib_opts = [lib_opts, ] * n
elif len(lib_opts) == 1:
lib_opts = lib_opts * n
if isinstance(metadata, dict):
metadata = [metadata, ] * n
elif len(metadata) == 1:
metadata = metadata * n
for r in range(n):
s[r].codec_opts = NULL
config = conf[r]
if 'pix_fmt_out' not in config or not config['pix_fmt_out']:
config['pix_fmt_out'] = config['pix_fmt_in']
if 'width_out' not in config or not config['width_out']:
config['width_out'] = config['width_in']
if 'height_out' not in config or not config['height_out']:
config['height_out'] = config['height_in']
if 'codec' not in config or not config['codec']:
s[r].codec_id = self.fmt_ctx.oformat.video_codec
codec_desc = avcodec_find_encoder(s[r].codec_id)
if codec_desc == NULL:
raise Exception('Default codec not found for output file.')
config['codec'] = codec_desc.name
else:
codec_desc = avcodec_find_encoder_by_name(config['codec'])
if codec_desc == NULL:
self.clean_up()
raise Exception('Encoder codec %s not available.' % config['codec'])
s[r].codec_id = codec_desc.id
s[r].width_in = config['width_in']
s[r].width_out = config['width_out']
s[r].height_in = config['height_in']
s[r].height_out = config['height_out']
s[r].num, s[r].den = config['frame_rate']
if av_get_pix_fmt(config['pix_fmt_in']) == AV_PIX_FMT_NONE:
self.clean_up()
raise Exception('Pixel format %s not found.' % config['pix_fmt_in'])
if av_get_pix_fmt(config['pix_fmt_out']) == AV_PIX_FMT_NONE:
self.clean_up()
raise Exception('Pixel format %s not found.' % config['pix_fmt_out'])
s[r].pix_fmt_in = av_get_pix_fmt(config['pix_fmt_in'])
s[r].pix_fmt_out = av_get_pix_fmt(config['pix_fmt_out'])
s[r].codec = avcodec_find_encoder(s[r].codec_id)
if s[r].codec == NULL:
self.clean_up()
raise Exception('Codec %s not found.' % config['codec'])
s[r].av_stream = avformat_new_stream(self.fmt_ctx, NULL)
if s[r].av_stream == NULL:
self.clean_up()
raise Exception("Couldn't create stream %d." % r)
s[r].index = s[r].av_stream.index
s[r].codec_ctx = avcodec_alloc_context3(s[r].codec)
if s[r].codec_ctx == NULL:
self.clean_up()
raise MemoryError("Couldn't create stream %d." % r)
s[r].codec_ctx.width = s[r].width_out
s[r].codec_ctx.height = s[r].height_out
supported_rates = get_supported_framerates(config['codec'], (s[r].num, s[r].den))
if supported_rates and supported_rates[0] != (s[r].num, s[r].den):
self.clean_up()
raise Exception('%d/%d is not a supported frame rate for codec %s, the \
closest valid rate is %d/%d' % (s[r].num, s[r].den, config['codec'],
supported_rates[0][0], supported_rates[0][1]))
s[r].av_stream.avg_frame_rate.num = s[r].num
s[r].av_stream.avg_frame_rate.den = s[r].den
s[r].av_stream.r_frame_rate.num = s[r].num
s[r].av_stream.r_frame_rate.den = s[r].den
s[r].codec_ctx.time_base.den = s[r].num
s[r].codec_ctx.time_base.num = s[r].den
s[r].codec_ctx.pix_fmt = s[r].pix_fmt_out
for k, v in metadata[r].items():
k_b = k.encode('utf8')
res = av_dict_set(&s[r].av_stream.metadata, k_b, v, 0)
if res < 0:
av_dict_free(&s[r].av_stream.metadata)
self.clean_up()
raise Exception('Failed to set option %s: %s for stream %d; %s'
% (k, v, r, tcode(emsg(res, msg, sizeof(msg)))))
# Some formats want stream headers to be separate
if self.fmt_ctx.oformat.flags & AVFMT_GLOBALHEADER:
s[r].codec_ctx.flags |= AV_CODEC_FLAG_GLOBAL_HEADER
supported_fmts = get_supported_pixfmts(config['codec'], config['pix_fmt_out'])
if supported_fmts and supported_fmts[0] != config['pix_fmt_out'].decode('utf8'):
self.clean_up()
raise Exception('%s is not a supported pixel format for codec %s, the '
'best valid format is %s' % (config['pix_fmt_out'], config['codec'],
supported_fmts[0]))
if (s[r].codec_ctx.pix_fmt != s[r].pix_fmt_in or s[r].codec_ctx.width != s[r].width_in or
s[r].codec_ctx.height != s[r].height_in):
s[r].av_frame = av_frame_alloc()
if s[r].av_frame == NULL:
self.clean_up()
raise MemoryError()
s[r].av_frame.format = s[r].pix_fmt_out
s[r].av_frame.width = s[r].width_out
s[r].av_frame.height = s[r].height_out
if av_frame_get_buffer(s[r].av_frame, 32) < 0:
raise Exception('Cannot allocate frame buffers.')
s[r].sws_ctx = sws_getCachedContext(NULL, s[r].width_in, s[r].height_in,\
s[r].pix_fmt_in, s[r].codec_ctx.width, s[r].codec_ctx.height,\
s[r].codec_ctx.pix_fmt, SWS_BICUBIC, NULL, NULL, NULL)
if s[r].sws_ctx == NULL:
self.clean_up()
raise Exception('Cannot find conversion context.')
for k, v in lib_opts[r].items():
k_b = k.encode('utf8')
if opt_default(k_b, v, s[r].sws_ctx, NULL, NULL, NULL, &self.format_opts, &s[r].codec_opts) < 0:
raise Exception('library option %s: %s not found' % (k, v))
res = avcodec_open2(s[r].codec_ctx, s[r].codec, &s[r].codec_opts)
bad_vals = ''
dict_temp = av_dict_get(s[r].codec_opts, b"", dict_temp, AV_DICT_IGNORE_SUFFIX)
while dict_temp != NULL:
bad_vals += '%s: %s, ' % (dict_temp.key, dict_temp.value)
dict_temp = av_dict_get(s[r].codec_opts, b"", dict_temp, AV_DICT_IGNORE_SUFFIX)
av_dict_free(&s[r].codec_opts)
if bad_vals:
msg2 = ("The following options were not recognized: %s.\n" % bad_vals).encode('utf8')
av_log(NULL, AV_LOG_ERROR, '%s', msg2)
if res < 0:
self.clean_up()
raise Exception('Failed to open codec for stream %d; %s' % (r, tcode(emsg(res, msg, sizeof(msg)))))
res = avcodec_parameters_from_context(s[r].av_stream.codecpar, s[r].codec_ctx)
if res < 0:
self.clean_up()
raise Exception('Failed to initialize stream parameters for stream %d; %s' % (r, tcode(emsg(res, msg, sizeof(msg)))))
s[r].pts = 0
if self.fmt_ctx.oformat.flags & AVFMT_VARIABLE_FPS:
if self.fmt_ctx.oformat.flags & AVFMT_NOTIMESTAMPS:
s[r].sync_fmt = VSYNC_PASSTHROUGH
else:
s[r].sync_fmt = VSYNC_VFR
else:
s[r].sync_fmt = VSYNC_CFR
if not (self.fmt_ctx.oformat.flags & AVFMT_NOFILE):
res = avio_check(filename, 0)
if (not res) and not overwrite:
self.clean_up()
raise Exception('File %s already exists.' % filename)
elif res < 0 and res != AV_ENOENT:
self.clean_up()
raise Exception('File error: ' + tcode(emsg(res, msg, sizeof(msg))))
res = avio_open2(&self.fmt_ctx.pb, filename, AVIO_FLAG_WRITE, NULL, NULL)
if res < 0:
self.clean_up()
raise Exception('File error: ' + tcode(emsg(res, msg, sizeof(msg))))
res = avformat_write_header(self.fmt_ctx, &self.format_opts)
bad_vals = ''
dict_temp = av_dict_get(self.format_opts, b"", dict_temp, AV_DICT_IGNORE_SUFFIX)
while dict_temp != NULL:
bad_vals += '%s: %s, ' % (dict_temp.key, dict_temp.value)
dict_temp = av_dict_get(self.format_opts, "", dict_temp, AV_DICT_IGNORE_SUFFIX)
av_dict_free(&self.format_opts)
if bad_vals:
msg2 = ("The following options were not recognized: %s.\n" % bad_vals).encode('utf8')
av_log(NULL, AV_LOG_ERROR, '%s', msg2)
if res < 0:
self.clean_up()
raise Exception('Error writing header: ' + tcode(emsg(res, msg, sizeof(msg))))
def __dealloc__(self):
self.close()
cpdef close(self):
'''Closes the writer and writes any frames cached and not yet written.
Until called, or until the instance is deleted (and this is implicitly called)
the file is not fully written.
.. warning::
After calling this method, calling any other class method on this instance may
result in a crash or program corruption.
'''
cdef int r, res, wrote = 0
cdef char msg[256]
cdef AVPacket pkt
if self.closed:
return
self.closed = 1
with nogil:
if self.fmt_ctx == NULL or (not self.n_streams) or self.streams[0].codec_ctx == NULL:
self.clean_up()
with gil:
return
for r in range(self.n_streams):
if not self.streams[r].count:
continue
wrote = 1
av_init_packet(&pkt)
pkt.data = NULL
pkt.size = 0
# flush
res = avcodec_send_frame(self.streams[r].codec_ctx, NULL)
if res < 0:
with gil:
raise Exception('Error sending NULL frame: ' + tcode(emsg(res, msg, sizeof(msg))))
while True:
res = avcodec_receive_packet(self.streams[r].codec_ctx, &pkt)
if res < 0:
if res != AVERROR_EOF:
with gil:
raise Exception('Error getting encoded packet: ' + tcode(emsg(res, msg, sizeof(msg))))
break
if pkt.pts != AV_NOPTS_VALUE:
pkt.pts = av_rescale_q(pkt.pts, self.streams[r].codec_ctx.time_base, self.streams[r].av_stream.time_base)
if pkt.dts != AV_NOPTS_VALUE:
pkt.dts = av_rescale_q(pkt.dts, self.streams[r].codec_ctx.time_base, self.streams[r].av_stream.time_base)
pkt.stream_index = self.streams[r].av_stream.index
self.total_size += pkt.size
res = av_interleaved_write_frame(self.fmt_ctx, &pkt)
if res < 0:
with gil:
raise Exception('Error writing packet: ' + tcode(emsg(res, msg, sizeof(msg))))
if wrote:
av_write_trailer(self.fmt_ctx)
self.clean_up()
def write_frame(MediaWriter self, Image img, double pts, int stream=0):
'''Writes a :class:`ffpyplayer.pic.Image` frame to the specified stream.
If the input data is different than the frame written to disk in either
size or pixel format as specified when creating the stream, the frame
is converted before writing. But the input image must match the size and
format as that specified when creating this stream.
:Parameters:
`img`: :class:`ffpyplayer.pic.Image`
The :class:`ffpyplayer.pic.Image` instance containing the frame
to be written to disk.
`pts`: float
The timestamp of this frame in video time. E.g. 0.5
means the frame should be displayed by a player at 0.5 seconds after
the video started playing. In a sense, the frame rate defines which
timestamps are valid timestamps. However, this is not always the
case, so if timestamps are invalid for a particular format, they are
forced to valid values, if possible.
`stream`: int
The stream number to which to write this frame. Defaults to 0.
:returns:
(int): The approximate number of bytes written to disk so far for this file.
.. note::
This is not the same as the number of bytes passed to this function
so far, because the encoders cache data before writing to disk.
So although some frames may have been passed, the return value
may not represent this.
An extreme example is where the same frame is passed many times
to h264; the encoder will only write this frame once when the Writer
object is closed and encoders are flushed, so this function
will only return 0.
See :ref:`examples` for its usage.
'''
cdef int res = 0, got_pkt
cdef int frame_cloned = 0
cdef AVFrame *frame_in = img.frame
cdef AVFrame *frame_out
cdef MediaStream *s
cdef double ipts, dpts
cdef int64_t rounded_pts
cdef AVPacket pkt
cdef char msg[256]
if stream >= self.n_streams:
raise Exception('Invalid stream number %d' % stream)
s = self.streams + stream
if (frame_in.width != s.width_in or frame_in.height != s.height_in or
frame_in.format != <AVPixelFormat>s.pix_fmt_in):
raise Exception("Input image doesn't match stream specified parameters.")
with nogil:
if s.av_frame != NULL:
frame_out = s.av_frame
sws_scale(s.sws_ctx, <const uint8_t *const *>frame_in.data, frame_in.linesize,
0, frame_in.height, frame_out.data, frame_out.linesize)
else:
frame_out = av_frame_clone(frame_in)
frame_cloned = 1
if frame_out == NULL:
with gil:
raise MemoryError
rounded_pts = <int64_t>floor(pts / av_q2d(s.codec_ctx.time_base) + 0.5)
frame_out.pict_type = AV_PICTURE_TYPE_NONE
frame_out.pts = rounded_pts
av_init_packet(&pkt)
pkt.data = NULL
pkt.size = 0
res = avcodec_send_frame(s.codec_ctx, frame_out)
if res < 0:
if frame_cloned:
av_frame_free(&frame_out)
with gil:
raise Exception('Error sending frame: ' + tcode(emsg(res, msg, sizeof(msg))))
while True:
res = avcodec_receive_packet(s.codec_ctx, &pkt)
if res < 0:
if frame_cloned:
av_frame_free(&frame_out)
if res != AVERROR_EOF and res != AV_EAGAIN:
with gil:
raise Exception('Error getting encoded packet: ' + tcode(emsg(res, msg, sizeof(msg))))
break
if pkt.pts != AV_NOPTS_VALUE:
pkt.pts = av_rescale_q(pkt.pts, s.codec_ctx.time_base, s.av_stream.time_base)
if pkt.dts != AV_NOPTS_VALUE:
pkt.dts = av_rescale_q(pkt.dts, s.codec_ctx.time_base, s.av_stream.time_base)
pkt.stream_index = s.av_stream.index
self.total_size += pkt.size
res = av_interleaved_write_frame(self.fmt_ctx, &pkt)
if res < 0:
if frame_cloned:
av_frame_free(&frame_out)
with gil:
raise Exception('Error writing packet: ' + tcode(emsg(res, msg, sizeof(msg))))
s.pts += 1
s.count += 1
if frame_cloned:
av_frame_free(&frame_out)
return self.total_size
def get_configuration(self):
'''Returns the configuration parameters used to initialize all the streams for this
instance.
This is not the same as the dicts passed when creating the file because
this uses the actual parameters used.
:returns:
list: List of dicts for each stream.
For example:
.. code-block:: python
from ffpyplayer.writer import MediaWriter
w, h = 640, 480
out_opts = {'pix_fmt_in':'rgb24', 'width_in':w, 'height_in':h, 'codec':'rawvideo',
'frame_rate':(5, 1)}
writer = MediaWriter('output.avi', [out_opts] * 2, width_out=w/2, height_out=h/2)
print writer.get_configuration()
[{'height_in': 480, 'codec': 'rawvideo', 'width_in': 640, 'frame_rate': (5, 1),
'pix_fmt_in': 'rgb24', 'width_out': 320, 'height_out': 240, 'pix_fmt_out': 'rgb24'},
{'height_in': 480, 'codec': 'rawvideo', 'width_in': 640, 'frame_rate': (5, 1),
'pix_fmt_in': 'rgb24', 'width_out': 320, 'height_out': 240, 'pix_fmt_out': 'rgb24'}]
'''
return convert_to_str(deepcopy(self.config))
cdef void clean_up(MediaWriter self) nogil:
cdef int r
for r in range(self.n_streams):
# If the in and out formats are different we must delete the out frame data buffer
if self.streams[r].av_frame != NULL:
av_frame_free(&self.streams[r].av_frame)
self.streams[r].av_frame = NULL
if self.streams[r].sws_ctx != NULL:
sws_freeContext(self.streams[r].sws_ctx)
self.streams[r].sws_ctx= NULL
if self.streams[r].codec_opts:
av_dict_free(&self.streams[r].codec_opts)
if self.streams[r].codec_ctx:
avcodec_free_context(&self.streams[r].codec_ctx)
free(self.streams)
self.streams = NULL
self.n_streams = 0
if self.fmt_ctx != NULL:
if self.fmt_ctx.pb != NULL and not (self.fmt_ctx.oformat.flags & AVFMT_NOFILE):
avio_close(self.fmt_ctx.pb)
avformat_free_context(self.fmt_ctx)
self.fmt_ctx = NULL
av_dict_free(&self.format_opts)
self.total_size = 0