Compare commits

...

9 Commits

Author SHA1 Message Date
97dd6c691b license notice 2025-10-03 18:15:25 +02:00
b0c3fbc1b1 cleanup 2025-10-03 18:08:09 +02:00
7cf7a8cb9a license notice 2025-10-03 18:08:00 +02:00
5af462fea2 cleanup 2025-10-03 17:22:25 +02:00
417238626b cleanup 2025-10-03 17:16:14 +02:00
b31704420f decoder 2025-10-03 17:07:17 +02:00
dfd0902256 decoder 2025-10-03 16:55:38 +02:00
2316435af8 demuxer, decoder not working 2025-10-03 15:10:49 +02:00
3c56a880da demuxer 2025-10-03 13:25:14 +02:00
10 changed files with 555 additions and 1 deletions

2
.gitignore vendored
View File

@ -173,4 +173,4 @@ cython_debug/
# PyPI configuration file # PyPI configuration file
.pypirc .pypirc
/test.mp4

15
mp4/__init__.py Normal file
View File

@ -0,0 +1,15 @@
# People's Video Editor: high quality, GPU accelerated mp4 editor
# Copyright (C) 2025 Roz K <roz@rozk.net>
#
# This file is part of People's Video Editor.
#
# People's Video Editor is free software: you can redistribute it and/or modify it under the terms of the
# GNU General Public License as published by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
#
# People's Video Editor is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with People's Video Editor.
# If not, see <https://www.gnu.org/licenses/>.

29
mp4/codec.py Normal file
View File

@ -0,0 +1,29 @@
# People's Video Editor: high quality, GPU accelerated mp4 editor
# Copyright (C) 2025 Roz K <roz@rozk.net>
#
# This file is part of People's Video Editor.
#
# People's Video Editor is free software: you can redistribute it and/or modify it under the terms of the
# GNU General Public License as published by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
#
# People's Video Editor is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with People's Video Editor.
# If not, see <https://www.gnu.org/licenses/>.
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.decode("utf-8")

61
mp4/decoder.py Normal file
View File

@ -0,0 +1,61 @@
# People's Video Editor: high quality, GPU accelerated mp4 editor
# Copyright (C) 2025 Roz K <roz@rozk.net>
#
# This file is part of People's Video Editor.
#
# People's Video Editor is free software: you can redistribute it and/or modify it under the terms of the
# GNU General Public License as published by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
#
# People's Video Editor is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with People's Video Editor.
# If not, see <https://www.gnu.org/licenses/>.
from . import libav
from .packet import Packet
from .frame import Frame
class Decoder:
__slots__ = '_context'
def __init__(self, stream):
self._context = libav.codec_alloc_context(stream.codec)
if not self._context:
raise MemoryError
errcode = libav.codec_parameters_to_context(self._context, stream.parameters)
if errcode < 0:
libav.codec_free_context(self._context)
raise Exception("Failed to set context parameters")
errcode = libav.codec_open(self._context, stream.codec)
if errcode < 0:
libav.codec_free_context(self._context)
raise Exception("Failed to open codec context")
def __del__(self):
if self._context:
libav.codec_free_context(self._context)
def _receive(self):
frames = []
while True:
frame = Frame()
errcode = libav.codec_receive_frame(self._context, frame)
if errcode in (libav.AVERROR_EOF, libav.AVERROR_EAGAIN):
break
elif errcode < 0:
errstring = libav.strerror(errcode)
raise Exception(f"Failed to receive frame: {errstring}")
frames.append(frame)
return frames
def decode(self, packet):
if not self._context:
return None
errcode = libav.codec_send_packet(self._context, packet)
if errcode < 0:
errstring = libav.strerror(errcode)
raise Exception(f"Failed to send packet: {errstring}")
return self._receive()

69
mp4/demuxer.py Normal file
View File

@ -0,0 +1,69 @@
# People's Video Editor: high quality, GPU accelerated mp4 editor
# Copyright (C) 2025 Roz K <roz@rozk.net>
#
# This file is part of People's Video Editor.
#
# People's Video Editor is free software: you can redistribute it and/or modify it under the terms of the
# GNU General Public License as published by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
#
# People's Video Editor is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with People's Video Editor.
# If not, see <https://www.gnu.org/licenses/>.
from . import libav
from .codec import Codec
from .stream import Stream
from .packet import Packet
class Demuxer:
__slots__ = '_context', 'video_stream', 'audio_stream'
def __init__(self, path):
self._context = libav.format_alloc_context()
if not self._context:
raise MemoryError
errcode = libav.format_open_input(self._context, "file:" + path)
if errcode < 0:
raise Exception(f"Failed to open: {path}")
errcode = libav.format_find_stream_info(self._context)
if errcode < 0:
libav.format_close_input(self._context)
raise Exception("Failed to find stream info")
self.video_stream = self._find_stream(libav.AVMEDIA_TYPE_VIDEO)
if self.video_stream is None:
libav.format_close_input(self._context)
raise Exception("Failed to find a video stream")
self.audio_stream = self._find_stream(libav.AVMEDIA_TYPE_AUDIO)
if self.audio_stream is None:
libav.format_close_input(self._context)
raise Exception("Failed to find an audio stream")
def __del__(self):
if self._context:
libav.format_close_input(self._context)
def _find_stream(self, type):
index, codec_ref = libav.format_find_best_stream(self._context, type)
if index < 0 or not codec_ref:
return None
parameters = self._context.contents.streams[index].contents.codecpar
return Stream(index, Codec(codec_ref), parameters)
@property
def nb_streams(self):
if not self._context:
return 0
return self._context.contents.nb_streams
def read_packet(self):
if not self._context:
return None
packet = Packet()
errcode = libav.read_frame(self._context, packet)
if errcode < 0:
return None
return packet

33
mp4/frame.py Normal file
View File

@ -0,0 +1,33 @@
# People's Video Editor: high quality, GPU accelerated mp4 editor
# Copyright (C) 2025 Roz K <roz@rozk.net>
#
# This file is part of People's Video Editor.
#
# People's Video Editor is free software: you can redistribute it and/or modify it under the terms of the
# GNU General Public License as published by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
#
# People's Video Editor is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with People's Video Editor.
# If not, see <https://www.gnu.org/licenses/>.
from . import libav
class Frame:
__slots__ = '_ref'
def __init__(self):
self._ref = libav.frame_alloc()
if not self._ref:
raise MemoryError
def __del__(self):
if self._ref:
libav.frame_free(self._ref)
@property
def _as_parameter_(self):
return self._ref

243
mp4/libav.py Normal file
View File

@ -0,0 +1,243 @@
# People's Video Editor: high quality, GPU accelerated mp4 editor
# Copyright (C) 2025 Roz K <roz@rozk.net>
#
# This file is part of People's Video Editor.
#
# People's Video Editor is free software: you can redistribute it and/or modify it under the terms of the
# GNU General Public License as published by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
#
# People's Video Editor is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with People's Video Editor.
# If not, see <https://www.gnu.org/licenses/>.
import errno
import ctypes
_avutil = ctypes.cdll.LoadLibrary('libavutil.so')
_avformat = ctypes.cdll.LoadLibrary('libavformat.so')
_avcodec = ctypes.cdll.LoadLibrary('libavcodec.so')
def _errtag(a, b, c, d):
return -(ord(a) | (ord(b) << 8) | (ord(c) << 16) | (ord(d) << 24))
if errno.EAGAIN < 0:
AVERROR_EAGAIN = errno.EAGAIN
else:
AVERROR_EAGAIN = -errno.EAGAIN
AVERROR_EOF = _errtag('E', 'O', 'F', ' ')
_AV_ERROR_MAX_STRING_SIZE = 64
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 AVFrame(ctypes.Structure):
pass
AVFrame_p = ctypes.POINTER(AVFrame)
AVFrame_pp = ctypes.POINTER(AVFrame_p)
class AVCodecParameters(ctypes.Structure):
pass
AVCodecParameters_p = ctypes.POINTER(AVCodecParameters)
class AVRational(ctypes.Structure):
_fields_ = [
("num", ctypes.c_int),
("den", ctypes.c_int)]
class AVStream(ctypes.Structure):
_fields_ = [
("av_class", ctypes.c_void_p),
("index", ctypes.c_int),
("id", ctypes.c_int),
("codecpar", AVCodecParameters_p),
("priv_data", ctypes.c_void_p),
("time_base", AVRational)]
# ...
AVStream_p = ctypes.POINTER(AVStream)
AVStream_pp = ctypes.POINTER(AVStream_p)
class AVFormatContext(ctypes.Structure):
_fields_ = [
("av_class", ctypes.c_void_p),
("iformat", ctypes.c_void_p),
("oformat", ctypes.c_void_p),
("priv_data", ctypes.c_void_p),
("pb", ctypes.c_void_p),
("ctx_flags", ctypes.c_int),
("nb_streams", ctypes.c_uint),
("streams", AVStream_pp)]
# ...
AVFormatContext_p = ctypes.POINTER(AVFormatContext)
AVFormatContext_pp = ctypes.POINTER(AVFormatContext_p)
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)
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)
class AVCodecContext(ctypes.Structure):
pass
AVCodecContext_p = ctypes.POINTER(AVCodecContext)
AVCodecContext_pp = ctypes.POINTER(AVCodecContext_p)
_avutil.av_strerror.restype = ctypes.c_int
_avutil.av_strerror.argtypes = [
ctypes.c_int, # errno
ctypes.c_char_p, # errbuf
ctypes.c_size_t] # errbuff_size
def strerror(errno):
errbuf = ctypes.create_string_buffer(_AV_ERROR_MAX_STRING_SIZE)
_avutil.av_strerror(errno, errbuf, _AV_ERROR_MAX_STRING_SIZE)
return errbuf.value.decode("utf-8")
_avutil.av_frame_alloc.restype = AVFrame_p
_avutil.av_frame_alloc.argtypes = None
def frame_alloc():
return _avutil.av_frame_alloc()
_avutil.av_frame_free.restype = None
_avutil.av_frame_free.argtypes = [AVFrame_pp]
def frame_free(frame):
_avutil.av_frame_free(ctypes.byref(frame))
_avformat.avformat_alloc_context.restype = AVFormatContext_p
_avformat.avformat_alloc_context.argtypes = None
def format_alloc_context():
return _avformat.avformat_alloc_context()
_avformat.avformat_free_context.restype = None
_avformat.avformat_free_context.argtypes = [AVFormatContext_p]
def format_free_context(context):
_avformat.avformat_free_context(context)
_avformat.avformat_open_input.restype = ctypes.c_int
_avformat.avformat_open_input.argtypes = [
AVFormatContext_pp,
ctypes.c_char_p, # url
ctypes.c_void_p, # format
ctypes.POINTER(ctypes.c_void_p)] # options
def format_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]
def format_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,
ctypes.POINTER(ctypes.c_void_p)] # options
def format_find_stream_info(context):
return _avformat.avformat_find_stream_info(context, None)
_avformat.av_find_best_stream.restype = ctypes.c_int
_avformat.av_find_best_stream.argtypes = [
AVFormatContext_p,
ctypes.c_int, # type
ctypes.c_int, # wanted stream
ctypes.c_int, # related stream
AVCodec_pp,
ctypes.c_int] # flags
def format_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
_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]
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, AVPacket_p]
def read_frame(context, packet):
return _avformat.av_read_frame(context, packet)
_avcodec.avcodec_alloc_context3.restype = AVCodecContext_p
_avcodec.avcodec_alloc_context3.argtypes = [AVCodec_p]
def codec_alloc_context(codec):
return _avcodec.avcodec_alloc_context3(codec)
_avcodec.avcodec_free_context.restype = None
_avcodec.avcodec_free_context.argtypes = [AVCodecContext_pp]
def codec_free_context(context):
_avcodec.avcodec_free_context(ctypes.byref(context))
_avcodec.avcodec_parameters_to_context.restype = ctypes.c_int
_avcodec.avcodec_parameters_to_context.argtypes = [AVCodecContext_p, AVCodecParameters_p]
def codec_parameters_to_context(context, parameters):
return _avcodec.avcodec_parameters_to_context(context, parameters)
_avcodec.avcodec_open2.restype = ctypes.c_int
_avcodec.avcodec_open2.argtypes = [
AVCodecContext_p,
AVCodec_p,
ctypes.POINTER(ctypes.c_void_p)] # options
def codec_open(context, codec):
return _avcodec.avcodec_open2(context, codec, None)
_avcodec.avcodec_send_packet.restype = ctypes.c_int
_avcodec.avcodec_send_packet.argtypes = [AVCodecContext_p, AVPacket_p]
def codec_send_packet(context, packet):
return _avcodec.avcodec_send_packet(context, packet)
_avcodec.avcodec_receive_frame.restype = ctypes.c_int
_avcodec.avcodec_receive_frame.argtypes = [AVCodecContext_p, AVFrame_p]
def codec_receive_frame(context, frame):
return _avcodec.avcodec_receive_frame(context, frame)

39
mp4/packet.py Normal file
View File

@ -0,0 +1,39 @@
# People's Video Editor: high quality, GPU accelerated mp4 editor
# Copyright (C) 2025 Roz K <roz@rozk.net>
#
# This file is part of People's Video Editor.
#
# People's Video Editor is free software: you can redistribute it and/or modify it under the terms of the
# GNU General Public License as published by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
#
# People's Video Editor is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with People's Video Editor.
# If not, see <https://www.gnu.org/licenses/>.
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

26
mp4/stream.py Normal file
View File

@ -0,0 +1,26 @@
# People's Video Editor: high quality, GPU accelerated mp4 editor
# Copyright (C) 2025 Roz K <roz@rozk.net>
#
# This file is part of People's Video Editor.
#
# People's Video Editor is free software: you can redistribute it and/or modify it under the terms of the
# GNU General Public License as published by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
#
# People's Video Editor is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with People's Video Editor.
# If not, see <https://www.gnu.org/licenses/>.
class Stream:
__slots__ = 'index', 'codec', 'parameters'
def __init__(self, index, codec, parameters):
self.index = index
self.codec = codec
self.parameters = parameters
def contains(self, packet):
return (self.index == packet.stream_index)

39
pve.py Normal file
View File

@ -0,0 +1,39 @@
# People's Video Editor: high quality, GPU accelerated mp4 editor
# Copyright (C) 2025 Roz K <roz@rozk.net>
#
# This file is part of People's Video Editor.
#
# People's Video Editor is free software: you can redistribute it and/or modify it under the terms of the
# GNU General Public License as published by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
#
# People's Video Editor is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with People's Video Editor.
# If not, see <https://www.gnu.org/licenses/>.
from mp4.demuxer import Demuxer
from mp4.decoder import Decoder
demuxer = Demuxer('test.mp4')
print(f"nb_streams = {demuxer.nb_streams}")
print(f"video codec = {demuxer.video_stream.codec.name}")
print(f"audio codec = {demuxer.audio_stream.codec.name}")
video_decoder = Decoder(demuxer.video_stream)
audio_decoder = Decoder(demuxer.audio_stream)
while True:
packet = demuxer.read_packet()
eof = (packet is None)
if eof or demuxer.video_stream.contains(packet):
video_frames = video_decoder.decode(packet)
print(f"decoded {len(video_frames)} video frames")
if eof or demuxer.audio_stream.contains(packet):
audio_frames = audio_decoder.decode(packet)
print(f"decoded {len(audio_frames)} audio frames")
if eof:
break