852 lines
30 KiB
Cython
852 lines
30 KiB
Cython
|
|
'''
|
||
|
|
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)
|