Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Instance: add a set_logger method #273

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion generator/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ def parse_param(cls, param_raw):
assert('const' not in param_raw)

# normalize spaces
param_raw = re.sub('\s+', ' ', param_raw)
param_raw = re.sub(r'\s+', ' ', param_raw)

split_value = param_raw.split(' ')
if len(split_value) > 1:
Expand Down
33 changes: 33 additions & 0 deletions generator/templates/header.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import os
import sys
import functools
import platform

# Used by EventManager in override.py
import inspect as _inspect
Expand Down Expand Up @@ -216,6 +217,24 @@ def find_lib():
# plugin_path used on win32 and MacOS in override.py
dll, plugin_path = find_lib()

def find_libc():
"""Return an instance of the loaded standard C library or raise an error if
the library has not been found.

This function should be compatible with Linux, Windows and macOS.
"""
system = platform.system()
if system == 'Windows':
# On Windows, msvcrt provides MS standard C
return ctypes.cdll.msvcrt
elif system == 'Linux' or system == 'Darwin':
libc_path = find_library('c')
if libc_path is None:
raise NotImplementedError('Cannot find a proper standard C library')
return ctypes.CDLL(libc_path)
garfvl marked this conversation as resolved.
Show resolved Hide resolved
else:
raise NotImplementedError('Cannot find a proper standard C library (Unsupported platform)')

class VLCException(Exception):
"""Exception raised by libvlc methods.
"""
Expand Down Expand Up @@ -472,6 +491,20 @@ class EventUnion(ctypes.Union):
('new_length', ctypes.c_longlong),
]

def loglevel_to_logging(level):
"""Converts VLC log level to python logging Log level
"""
if level == LogLevel.DEBUG:
return logging.DEBUG
elif level == LogLevel.ERROR:
return logging.ERROR
elif level == LogLevel.NOTICE:
return logging.INFO
elif level == LogLevel.WARNING:
return logging.WARNING
else:
return logging.INFO

# Generated structs #
# GENERATED_STRUCTS go here # see generate.py
# End of generated structs #
Expand Down
52 changes: 52 additions & 0 deletions generator/templates/override.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class Instance:
- a list of strings as first parameters
- the parameters given as the constructor parameters (must be strings)
"""

def __new__(cls, *args):
if len(args) == 1:
# Only 1 arg. It is either a C pointer, or an arg string,
Expand Down Expand Up @@ -142,6 +143,57 @@ def video_filter_list_get(self):
"""
return module_description_list(libvlc_video_filter_list_get(self))

def set_logger(self, logger, max_log_message_size=4096):
"""Links a logging.Logger object to the libVLC Instance

Along with the log level and message, each libVLC log will also include
The following extra info:
- vlc_module: the name of the VLC module (str).
- file: the VLC source filename (str).
- line: the VLC source file line number (int).
These variables can be used in the logger formatter.

@param logger: a logging.Logger object
@param max_log_message_size: defines the maximum size that will be
copied from VLC log messages. If you experience truncated log
messages, raise this number (default 4096).
"""
# libVLC provides the log message through a printf format + va_list.
# Unfortunately, there is no simple way to use a
# printf format + va_list in Python outside of the use of a C format
# function.
# As there is no guarantee to have access to a C `vasprintf`, we use
# `vsnprintf` with a log message max size.
libc = find_libc()
self._vsnprintf = libc.vsnprintf
self._max_log_message_size = max_log_message_size
self._logger = logger
# The log_handler is meant to be the "real" callback for libvlc_log_set().
@CallbackDecorators.LogCb
def log_handler(instance, log_level, ctx, fmt, va_list):
bufferString = ctypes.create_string_buffer(self._max_log_message_size)
self._vsnprintf(bufferString, self._max_log_message_size, fmt,
ctypes.cast(va_list, ctypes.c_void_p))
msg = bufferString.value.decode('utf-8')
module, file, line = libvlc_log_get_context(ctx)
module = module.decode('utf-8')
file = file.decode('utf-8')
self._logger.log(loglevel_to_logging(LogLevel(log_level)),
msg, extra={"vlc_module": module, "file": file, "line": line})
# We need to keep a reference to the log_handler function that persists
# after the end of the set_logger.
# If we do not do that, there is a (high) chance the python garbage
# collector will destroy the callback function and produce segfault in
# libVLC.
# To avoid that, link the log_handler lifetime to the Instance's one.
self._log_handler = log_handler
self.log_set(self._log_handler, None)

def get_logger(self):
"""Return the logger attached to the libVLC Instance (None by default)
"""
return getattr(self, '_logger', None)

class Media:
"""Create a new Media instance.

Expand Down
54 changes: 31 additions & 23 deletions tests/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import ctypes
import os
import unittest
import io
import re

try:
import urllib.parse as urllib # python3
Expand Down Expand Up @@ -203,33 +205,39 @@ def test_meta_get(self):
self.assertEqual(m.get_meta(vlc.Meta.Date), '2013')
self.assertEqual(m.get_meta(vlc.Meta.Genre), 'Sample')

def notest_log_get_context(self):
"""Semi-working test for log_get_context.

It crashes with a Segmentation fault after displaying some
messages. This should be fixed + a better test should be
devised so that we do not clutter the terminal.
"""
libc = ctypes.cdll.LoadLibrary("libc.{}".format("so.6" if os.uname()[0] == 'Linux' else "dylib"))
@vlc.CallbackDecorators.LogCb
def log_handler(instance, log_level, ctx, fmt, va_list):
bufferString = ctypes.create_string_buffer(4096)
libc.vsprintf(bufferString, fmt, ctypes.cast(va_list, ctypes.c_void_p))
msg = bufferString.value.decode('utf-8')
module, _file, _line = vlc.libvlc_log_get_context(ctx)
module = module.decode('utf-8')
try:
logger.warn(u"log_level={log_level}, module={module}, msg={msg}".format(log_level=log_level, module=module, msg=msg))
except Exception as e:
logger.exception(e)
import pdb; pdb.set_trace()

instance = vlc.Instance('--vout dummy --aout dummy')
instance.log_set(log_handler, None)
def test_set_logger(self):
vlc_logger = logging.Logger("VLC")
vlc_logger.setLevel(logging.DEBUG)
# push logs in memory
in_mem = io.StringIO()
handler = logging.StreamHandler(in_mem)
formatter = logging.Formatter('%(asctime)s;;;%(name)s;;;%(levelname)s;;;%(vlc_module)s;;;%(file)s;;;%(line)d;;;%(message)s')
handler.setFormatter(formatter)
vlc_logger.addHandler(handler)
# start in quiet mode: we cannot control how to logs are produced
# before setting the logger
instance = vlc.Instance('--vout dummy --aout dummy --quiet')
instance.set_logger(vlc_logger)
player = instance.media_player_new()
media = instance.media_new(SAMPLE)
player.set_media(media)
player.play()
player.stop()
handler.flush()
# check logs
# there can be a lot of log message to check
# just try to find a message we are sure to get
# Example:
# 2024-05-03 18:44:31,054;;;VLC;;;DEBUG;;;main;;;<src>;;;<line>;;;creating demux: access='file' demux='any' location='<path>' file='<path>'
pattern = r"(?P<asctime>.*);;;VLC;;;DEBUG;;;main;;;(?P<src>.*);;;(?P<line>.*);;;creating demux: access='file' demux='any' location='(?P<path1>.*)' file='(?P<path2>.*)'"
found = False
for line in in_mem.getvalue().splitlines():
#print(line)
m = re.match(pattern, line)
if m is not None:
found = True
break
self.assertEqual(found, True, "Cannot find a proper log message for demux creation")


if generate is not None:
Expand Down