diff --git a/.gitignore b/.gitignore index 36b13f1..ff1527d 100644 --- a/.gitignore +++ b/.gitignore @@ -173,4 +173,4 @@ cython_debug/ # PyPI configuration file .pypirc - +/test.mp4 diff --git a/mp4/__init__.py b/mp4/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mp4/codec.py b/mp4/codec.py new file mode 100644 index 0000000..ccb9e7f --- /dev/null +++ b/mp4/codec.py @@ -0,0 +1,20 @@ +# RozK + +class NullCodec: + @property + def name(self): + return b"" + +class Codec: + __slots__ = '_ref' + + def __init__(self, ref): + self._ref = ref + + @property + def _as_parameter_(self): + return self._ref + + @property + def name(self): + return self._ref.contents.name diff --git a/mp4/context.py b/mp4/context.py new file mode 100644 index 0000000..c6fbbcd --- /dev/null +++ b/mp4/context.py @@ -0,0 +1,54 @@ +# RozK + +from . import libav +from .codec import Codec +from .stream import NullStream, Stream +from .packet import Packet + +class Context: + __slots__ = '_ref' + + def __init__(self): + self._ref = libav.alloc_context() + if not self._ref: + raise MemoryError + + def __del__(self): + if self._ref: + libav.free_context(self._ref) + + @property + def _as_parameter_(self): + return self._ref + + def open_input(self, url): + if not self._ref: + return + errcode = libav.open_input(self._ref, url) + if errcode < 0: + raise Exception(f"Failed to open: {url}") + errcode = libav.find_stream_info(self._ref) + if errcode < 0: + libav.close_input(self._ref) + raise Exception("Failed to find stream info") + + def close_input(self): + if self._ref: + libav.close_input(self._ref) + + def find_stream(self, type): + if not self._ref: + return NullStream() + index, codec_ref = libav.find_best_stream(self._ref, type) + if index < 0 or not codec_ref: + return NullStream() + return Stream(index, Codec(codec_ref)) + + def read_packet(self): + if not self._ref: + return None + packet = Packet() + errcode = libav.read_frame(self._ref, packet) + if errcode < 0: + return None + return packet diff --git a/mp4/demuxer.py b/mp4/demuxer.py new file mode 100644 index 0000000..fd6d881 --- /dev/null +++ b/mp4/demuxer.py @@ -0,0 +1,19 @@ +# RozK + +from . import libav +from .context import Context + +class Demuxer: + __slots__ = 'context', 'video_stream', 'audio_stream' + + def __init__(self, path): + self.context = Context() + self.context.open_input("file:" + path) + self.video_stream = self.context.find_stream(libav.AVMEDIA_TYPE_VIDEO) + self.audio_stream = self.context.find_stream(libav.AVMEDIA_TYPE_AUDIO) + + def read_packet(self): + return self.context.read_packet() + + def close(self): + self.context.close_input() diff --git a/mp4/libav.py b/mp4/libav.py new file mode 100644 index 0000000..3890650 --- /dev/null +++ b/mp4/libav.py @@ -0,0 +1,112 @@ +# RozK +# https://www.ffmpeg.org/doxygen/trunk/group__libavf.html + +import ctypes + +_avformat = ctypes.cdll.LoadLibrary('libavformat.so') +_avcodec = ctypes.cdll.LoadLibrary('libavcodec.so') + +class AVFormatContext(ctypes.Structure): + pass + +AVFormatContext_p = ctypes.POINTER(AVFormatContext) +AVFormatContext_pp = ctypes.POINTER(AVFormatContext_p) + +_avformat.avformat_alloc_context.restype = AVFormatContext_p +_avformat.avformat_alloc_context.argtypes = None + +def alloc_context(): + return _avformat.avformat_alloc_context() + +_avformat.avformat_free_context.restype = None +_avformat.avformat_free_context.argtypes = [AVFormatContext_p] # context + +def free_context(context): + _avformat.avformat_free_context(context) + +_avformat.avformat_open_input.restype = ctypes.c_int +_avformat.avformat_open_input.argtypes = [ + AVFormatContext_pp, # context + ctypes.c_char_p, # url + ctypes.c_void_p, # format + ctypes.POINTER(ctypes.c_void_p)] # options + +def open_input(context, url): + return _avformat.avformat_open_input(ctypes.byref(context), url.encode('ascii', 'ignore'), None, None) + +_avformat.avformat_close_input.restype = None +_avformat.avformat_close_input.argtypes = [AVFormatContext_pp] # context + +def close_input(context): + _avformat.avformat_close_input(ctypes.byref(context)) + +_avformat.avformat_find_stream_info.restype = ctypes.c_int +_avformat.avformat_find_stream_info.argtypes = [ + AVFormatContext_p, # context + ctypes.POINTER(ctypes.c_void_p)] # options + +def find_stream_info(context): + return _avformat.avformat_find_stream_info(context, None) + +AVMEDIA_TYPE_UNKNOWN = -1 +AVMEDIA_TYPE_VIDEO = 0 +AVMEDIA_TYPE_AUDIO = 1 +AVMEDIA_TYPE_DATA = 2 +AVMEDIA_TYPE_SUBTITLE = 3 +AVMEDIA_TYPE_ATTACHMENT = 4 + +class AVCodec(ctypes.Structure): + _fields_ = [ + ("name", ctypes.c_char_p), + ("long_name", ctypes.c_char_p)] + # ... + +AVCodec_p = ctypes.POINTER(AVCodec) +AVCodec_pp = ctypes.POINTER(AVCodec_p) + +_avformat.av_find_best_stream.restype = ctypes.c_int +_avformat.av_find_best_stream.argtypes = [ + AVFormatContext_p, # context + ctypes.c_int, # type + ctypes.c_int, # wanted stream + ctypes.c_int, # related stream + AVCodec_pp, # decoder + ctypes.c_int] # flags + +def find_best_stream(context, type): + codec = AVCodec_p() + index = _avformat.av_find_best_stream(context, type, -1, -1, ctypes.byref(codec), 0) + return index, codec + +class AVPacket(ctypes.Structure): + _fields_ = [ + ("buf", ctypes.c_void_p), + ("pts", ctypes.c_int64), + ("dts", ctypes.c_int64), + ("data", ctypes.c_void_p), + ("size", ctypes.c_int), + ("stream_index", ctypes.c_int)] + # ... + +AVPacket_p = ctypes.POINTER(AVPacket) +AVPacket_pp = ctypes.POINTER(AVPacket_p) + +_avformat.av_packet_alloc.restype = AVPacket_p +_avformat.av_packet_alloc.argtypes = None + +def packet_alloc(): + return _avformat.av_packet_alloc() + +_avformat.av_packet_free.restype = None +_avformat.av_packet_free.argtypes = [AVPacket_pp] # packet + +def packet_free(packet): + _avformat.av_packet_free(ctypes.byref(packet)) + +_avformat.av_read_frame.restype = ctypes.c_int +_avformat.av_read_frame.argtypes = [ + AVFormatContext_p, # context + AVPacket_p] # packet + +def read_frame(context, packet): + return _avformat.av_read_frame(context, packet) diff --git a/mp4/packet.py b/mp4/packet.py new file mode 100644 index 0000000..c6aceab --- /dev/null +++ b/mp4/packet.py @@ -0,0 +1,25 @@ +# RozK + +from . import libav + +class Packet: + __slots__ = '_ref' + + def __init__(self): + self._ref = libav.packet_alloc() + if not self._ref: + raise MemoryError + + def __del__(self): + if self._ref: + libav.packet_free(self._ref) + + @property + def _as_parameter_(self): + return self._ref + + @property + def stream_index(self): + if self._ref: + return self._ref.contents.stream_index + return -1 diff --git a/mp4/stream.py b/mp4/stream.py new file mode 100644 index 0000000..25c0424 --- /dev/null +++ b/mp4/stream.py @@ -0,0 +1,25 @@ +# RozK + +from .codec import NullCodec + +class NullStream: + @property + def index(self): + return -1 + + @property + def codec(self): + return NullCodec() + + def contains(self, packet): + return False + +class Stream: + __slots__ = 'index', 'codec' + + def __init__(self, index, codec): + self.index = index + self.codec = codec + + def contains(self, packet): + return (self.index == packet.stream_index) diff --git a/pve.py b/pve.py new file mode 100644 index 0000000..b00aea0 --- /dev/null +++ b/pve.py @@ -0,0 +1,21 @@ +# RozK + +from mp4.demuxer import Demuxer + +demuxer = Demuxer('test.mp4') + +print(demuxer.video_stream.codec.name) +print(demuxer.audio_stream.codec.name) + +while True: + packet = demuxer.read_packet() + if packet is None: + break + if demuxer.video_stream.contains(packet): + continue + elif demuxer.audio_stream.contains(packet): + continue + else: + print("unkown packet") + +demuxer.close()