diff --git a/ffpyplayer/includes/ffmpeg.pxi b/ffpyplayer/includes/ffmpeg.pxi index 549089b..c5c13a3 100644 --- a/ffpyplayer/includes/ffmpeg.pxi +++ b/ffpyplayer/includes/ffmpeg.pxi @@ -25,6 +25,10 @@ cdef: int size int stream_index int flags + AVPacket *av_packet_alloc() + void av_packet_free(AVPacket **pkt) + void av_packet_rescale_ts(AVPacket *pkt, AVRational tb_src, AVRational tb_dst) + enum AVMediaType: AVMEDIA_TYPE_UNKNOWN = -1, #///< Usually treated as AVMEDIA_TYPE_DATA AVMEDIA_TYPE_VIDEO, @@ -44,7 +48,9 @@ cdef: extern from "libavformat/avio.h" nogil: int AVIO_FLAG_WRITE + int avio_closep(AVIOContext **s) int avio_check(const char *, int) + int avio_open(AVIOContext **s, const char *url, int flags) int avio_open2(AVIOContext **, const char *, int, const AVIOInterruptCB *, AVDictionary **) int avio_close(AVIOContext *) @@ -59,6 +65,14 @@ cdef: extern from "libavutil/eval.h" nogil: double av_strtod(const char *, char **) + + extern from "libavutil/mem.h" nogil: + void *av_calloc(size_t nmemb, size_t size) + void *av_malloc_array(size_t nmemb, size_t size) + + extern from "libavutil/avutil.h": + cdef int AVERROR_UNKNOWN + cdef int AVERROR_INVALIDDATA extern from "libavutil/avstring.h" nogil: size_t av_strlcpy(char *, const char *, size_t) @@ -67,9 +81,19 @@ cdef: extern from "libavutil/display.h" nogil: double av_display_rotation_get (const int32_t []) + + cdef extern from "libavutil/common.h": + cdef enum AVRounding: + AV_ROUND_ZERO = 0 + AV_ROUND_INF = 1 + AV_ROUND_DOWN = 2 + AV_ROUND_UP = 3 + AV_ROUND_NEAR_INF = 5 + AV_ROUND_PASS_MINMAX = 8191 extern from "libavutil/mathematics.h" nogil: int64_t av_rescale_q(int64_t, AVRational, AVRational) + int64_t av_rescale_q_rnd(int64_t a, AVRational bq, AVRational cq, AVRounding rnd) extern from "libavutil/pixdesc.h" nogil: struct AVPixFmtDescriptor: @@ -133,11 +157,16 @@ cdef: int av_clip(int a, int amin, int amax) int64_t AV_CH_LAYOUT_STEREO_DOWNMIX + int64_t AV_CH_LAYOUT_STEREO + int64_t AV_CH_LAYOUT_MONO + struct AVRational: int num #///< numerator int den #///< denominator double av_q2d(AVRational) + AVRational av_inv_q(AVRational q) int av_find_nearest_q_idx(AVRational, const AVRational*) + AVRational av_make_q (int num, int den) int AV_LOG_QUIET int AV_LOG_PANIC @@ -216,6 +245,8 @@ cdef: int AVFMT_NOTIMESTAMPS int AVFMT_NOFILE int AVFMT_RAWPICTURE + int AVSEEK_FLAG_BACKWARD + struct AVChapter: int id AVRational time_base @@ -230,6 +261,7 @@ cdef: const char *name const char *long_name const char *extensions + int av_seek_frame (AVFormatContext *s, int stream_index, int64_t timestamp, int flags) struct AVCodecTag: pass struct AVOutputFormat: @@ -388,6 +420,7 @@ cdef: const AVRational *supported_framerates const AVPixelFormat *pix_fmts AVMediaType type + AVSampleFormat *sample_fmts struct AVCodecContext: int width int height @@ -410,6 +443,9 @@ cdef: AVPixelFormat pix_fmt AVFrame *coded_frame AVRational pkt_timebase + AVRational framerate + void *priv_data + int64_t bit_rate struct AVCodecParameters: AVCodecID codec_id AVMediaType codec_type @@ -444,6 +480,7 @@ cdef: int channels int64_t pkt_pos AVBufferRef **buf + int64_t pkt_duration struct AVPicture: uint8_t **data int *linesize @@ -462,6 +499,7 @@ cdef: SUBTITLE_BITMAP SUBTITLE_TEXT SUBTITLE_ASS + int avcodec_parameters_copy(AVCodecParameters *dst, const AVCodecParameters *src) AVRational av_codec_get_pkt_timebase(const AVCodecContext *) int64_t av_frame_get_best_effort_timestamp(const AVFrame *) int av_codec_get_max_lowres(const AVCodec *) @@ -488,6 +526,8 @@ cdef: enum AVCodecID: AV_CODEC_ID_NONE AV_CODEC_ID_RAWVIDEO + AV_CODEC_ID_MP3 + AV_CODEC_ID_H264 AVCodec *avcodec_find_decoder(AVCodecID) AVCodec *avcodec_find_encoder(AVCodecID) AVCodec *avcodec_find_encoder_by_name(const char *) @@ -562,9 +602,11 @@ cdef: int av_buffersink_get_sample_rate(const AVFilterContext *) int av_buffersink_get_channels(const AVFilterContext *) uint64_t av_buffersink_get_channel_layout(const AVFilterContext *) + int av_buffersink_get_frame(AVFilterContext *ctx, AVFrame *frame) extern from "libavfilter/buffersrc.h" nogil: int av_buffersrc_add_frame(AVFilterContext *, AVFrame *) + int av_buffersrc_add_frame_flags (AVFilterContext *buffer_src, AVFrame *frame, int flags) extern from "clib/misc.h" nogil: uint8_t INDENT diff --git a/ffpyplayer/transcoder.pxd b/ffpyplayer/transcoder.pxd new file mode 100644 index 0000000..b74d938 --- /dev/null +++ b/ffpyplayer/transcoder.pxd @@ -0,0 +1,60 @@ +include 'includes/ffmpeg.pxi' + + +ctypedef struct FilteringContext: + AVFilterContext *buffersink_ctx + AVFilterContext *buffersrc_ctx + AVFilterGraph *filter_graph + AVPacket *enc_pkt + AVFrame *filtered_frame + + +ctypedef struct StreamContext: + AVCodecContext *dec_ctx + AVCodecContext *enc_ctx + AVFrame *dec_frame + + +cdef extern from "inttypes.h" nogil: + const char *PRId64 + const char *PRIx64 + + +cdef extern from "stdio.h" nogil: + int snprintf(char *, size_t, const char *, ... ) + + +cdef extern from "errno.h" nogil: + int ENOENT + int EAGAIN + + +cdef extern from "errno.h" nogil: + int ENOMEM + +cdef extern from "libavutil/error.h": + char* av_err2str(int errnum) + +cdef extern from "math.h" nogil: + double ceil(double x) + +cdef class Transcoder(object): + + cdef AVFormatContext *ifmt_ctx + cdef AVFormatContext *ofmt_ctx + cdef StreamContext *stream_ctx + cdef FilteringContext *filter_ctx + + cdef int _end(self, int ret, AVPacket * packet) nogil except 1 + cdef int _free_filters(self, int ret, AVFilterInOut *inputs, AVFilterInOut *outputs) nogil except 1 + + cdef int open_input_file(self, const char *filename) nogil except 1 + cdef int open_output_file(self, const char *filename, int output_width, int output_bitrate) nogil except 1 + cdef int init_filter(self, FilteringContext* fctx, AVCodecContext *dec_ctx, + AVCodecContext *enc_ctx, const char *filter_spec) nogil except 1 + cdef int init_filters(self, const char *video_filters) nogil except 1 + cdef int encode_write_frame(self, unsigned int stream_index, int flush) nogil except 1 + cdef int filter_encode_write_frame(self, AVFrame *frame, unsigned int stream_index) nogil except 1 + cdef int flush_encoder(self, unsigned int stream_index) nogil except 1 + + cdef int start_transcoding(self, const char *input_file, const char *output_file, const char *video_filters, int output_width, int output_bitrate) nogil except 1 diff --git a/ffpyplayer/transcoder.pyx b/ffpyplayer/transcoder.pyx new file mode 100644 index 0000000..208822b --- /dev/null +++ b/ffpyplayer/transcoder.pyx @@ -0,0 +1,601 @@ +# Cython implementation of video transcoding, based on example: +# https://github.com/FFmpeg/FFmpeg/blob/master/doc/examples/transcoding.c +from libc.stdlib cimport malloc, free + + +cdef class Transcoder(object): + + cdef int open_input_file(self, const char *filename) nogil except 1: + cdef int ret + cdef unsigned int i + + self.ifmt_ctx = NULL + + + cdef AVStream *stream + cdef AVCodec *dec + cdef AVCodecContext *codec_ctx = NULL + + + ret = avformat_open_input(&self.ifmt_ctx, filename, NULL, NULL) + if ret < 0: + with gil: + print("Unable to open the file") + return ret + + ret = avformat_find_stream_info(self.ifmt_ctx, NULL) + if ret < 0: + with gil: + print("Unable to find the stream information") + return ret + + self.stream_ctx = av_calloc(self.ifmt_ctx.nb_streams, sizeof(StreamContext)) + + if not self.stream_ctx: + return AVERROR(ENOMEM) + + for i in range(self.ifmt_ctx.nb_streams): + stream = self.ifmt_ctx.streams[i] + dec = avcodec_find_decoder(stream.codecpar.codec_id) + codec_ctx = NULL + + if not dec: + with gil: + print("Failed to find decoder for stream", i) + + codec_ctx = avcodec_alloc_context3(dec) + if not codec_ctx: + with gil: + print("Failed to allocate the decoder context for stream", i) + return AVERROR(ENOMEM) + + ret = avcodec_parameters_to_context(codec_ctx, stream.codecpar) + if ret < 0: + with gil: + print("Failed to copy decoder parameters to input decoder context for stream", i) + return ret + + + if (codec_ctx.codec_type == AVMEDIA_TYPE_VIDEO or codec_ctx.codec_type == AVMEDIA_TYPE_AUDIO): + + if codec_ctx.codec_type == AVMEDIA_TYPE_VIDEO: + codec_ctx.framerate = av_guess_frame_rate(self.ifmt_ctx, stream, NULL) + + + ret = avcodec_open2(codec_ctx, dec, NULL) + if ret < 0: + with gil: + print("Failed to open decoder for stream", i) + return ret + + self.stream_ctx[i].dec_ctx = codec_ctx + + self.stream_ctx[i].dec_frame = av_frame_alloc() + if not self.stream_ctx[i].dec_frame: + return AVERROR(ENOMEM) + + av_dump_format(self.ifmt_ctx, 0, filename, 0) + return 0 + + cdef int open_output_file(self, const char *filename, int output_width, int output_bitrate) nogil except 1: + cdef AVStream *out_stream + cdef AVStream *in_stream + cdef AVCodecContext *dec_ctx, *enc_ctx + cdef const AVCodec *encoder + cdef int ret + cdef unsigned int i + cdef int new_height + + + self.ofmt_ctx = NULL + avformat_alloc_output_context2(&self.ofmt_ctx, NULL, NULL, filename) + + if not self.ofmt_ctx: + with gil: + print("Could not create output context") + return AVERROR_UNKNOWN + + for i in range(self.ifmt_ctx.nb_streams): + dec_ctx = self.stream_ctx[i].dec_ctx + if not (dec_ctx.codec_type == AVMEDIA_TYPE_VIDEO or dec_ctx.codec_type == AVMEDIA_TYPE_AUDIO): + continue + + out_stream = avformat_new_stream(self.ofmt_ctx, NULL) + if not out_stream: + with gil: + print("Failed allocating output stream") + return AVERROR_UNKNOWN + + in_stream = self.ifmt_ctx.streams[i] + + + encoder = avcodec_find_encoder(dec_ctx.codec_id) + if not encoder: + with gil: + print("Necessary encoder not found") + return AVERROR_INVALIDDATA + + enc_ctx = avcodec_alloc_context3(encoder) + + if not enc_ctx: + with gil: + print("Failed to allocate the encoder context") + return AVERROR(ENOMEM) + + if dec_ctx.codec_type == AVMEDIA_TYPE_VIDEO: + enc_ctx.bit_rate = output_bitrate * 1000 # ~ 4000 kbps + enc_ctx.width = output_width + + new_height = (dec_ctx.height * enc_ctx.width) // dec_ctx.width + if new_height % 2 != 0: + new_height += 1 + enc_ctx.height = new_height + + enc_ctx.pix_fmt = AV_PIX_FMT_YUV420P + enc_ctx.time_base = dec_ctx.time_base + av_opt_set(enc_ctx.priv_data, "preset", "ultrafast", 0) + av_opt_set(enc_ctx.priv_data, "profile", "baseline", 0) + av_opt_set(enc_ctx.priv_data, "level", "4.0", 0) + # av_opt_set(enc_ctx.priv_data, "crf", "30", 0) + + else: + # take first format from list of supported formats + enc_ctx.sample_fmt = encoder.sample_fmts[0] + enc_ctx.sample_rate = dec_ctx.sample_rate + enc_ctx.channel_layout = AV_CH_LAYOUT_STEREO + enc_ctx.channels = 2 + enc_ctx.time_base.num = 1 + enc_ctx.time_base.den = enc_ctx.sample_rate + + if self.ofmt_ctx.oformat.flags & AVFMT_GLOBALHEADER: + enc_ctx.flags |= AV_CODEC_FLAG_GLOBAL_HEADER + + ret = avcodec_open2(enc_ctx, encoder, NULL) + if ret < 0: + with gil: + print("Cannot open video encoder for stream", i, {av_err2str(ret)}) + return ret + + ret = avcodec_parameters_from_context(out_stream.codecpar, enc_ctx) + if ret < 0: + with gil: + print("Failed to copy encoder parameters to output stream", i) + return ret + + out_stream.time_base = enc_ctx.time_base + self.stream_ctx[i].enc_ctx = enc_ctx + + av_dump_format(self.ofmt_ctx, 0, filename, 1) + + if not (self.ofmt_ctx.oformat.flags & AVFMT_NOFILE): + ret = avio_open(&self.ofmt_ctx.pb, filename, AVIO_FLAG_WRITE) + if ret < 0: + with gil: + print("Could not open output file", filename) + return ret + + ret = avformat_write_header(self.ofmt_ctx, NULL) + if ret < 0: + with gil: + print("Error occurred when opening output file") + return ret + + + return 0 + + cdef int _free_filters(self, int ret, AVFilterInOut *inputs, AVFilterInOut *outputs) nogil except 1: + avfilter_inout_free(&inputs) + avfilter_inout_free(&outputs) + return ret + + cdef int init_filter(self, FilteringContext* fctx, AVCodecContext *dec_ctx, AVCodecContext *enc_ctx, const char *filter_spec) nogil except 1: + cdef char args[512] + cdef int ret = 0 + cdef const AVFilter *buffersrc = NULL + cdef const AVFilter *buffersink = NULL + cdef AVFilterContext *buffersrc_ctx = NULL + cdef AVFilterContext *buffersink_ctx = NULL + cdef AVFilterInOut *outputs = avfilter_inout_alloc() + cdef AVFilterInOut *inputs = avfilter_inout_alloc() + cdef AVFilterGraph *filter_graph = avfilter_graph_alloc() + + + if not outputs or not inputs or not filter_graph: + ret = AVERROR(ENOMEM) + self._free_filters(ret, inputs, outputs) + + if dec_ctx.codec_type == AVMEDIA_TYPE_VIDEO: + buffersrc = avfilter_get_by_name("buffer") + buffersink = avfilter_get_by_name("buffersink") + + if not buffersrc or not buffersink: + with gil: + print("filtering source or sink element not found") + ret = AVERROR_UNKNOWN + self._free_filters(ret, inputs, outputs) + + snprintf(args, sizeof(args), + "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d", + dec_ctx.width, dec_ctx.height, dec_ctx.pix_fmt, + dec_ctx.time_base.num, dec_ctx.time_base.den, + dec_ctx.sample_aspect_ratio.num, + dec_ctx.sample_aspect_ratio.den + ) + + ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in", args, NULL, filter_graph) + if ret < 0: + with gil: + print("Cannot create buffer source") + self._free_filters(ret, inputs, outputs) + + ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", NULL, NULL, filter_graph) + if ret < 0: + with gil: + print("Cannot create buffer sink") + self._free_filters(ret, inputs, outputs) + + ret = av_opt_set_bin(buffersink_ctx, "pix_fmts", &enc_ctx.pix_fmt, sizeof(enc_ctx.pix_fmt), AV_OPT_SEARCH_CHILDREN) + if ret < 0: + with gil: + print("Cannot set output pixel format") + self._free_filters(ret, inputs, outputs) + + elif dec_ctx.codec_type == AVMEDIA_TYPE_AUDIO: + buffersrc = avfilter_get_by_name("abuffer") + buffersink = avfilter_get_by_name("abuffersink") + if not buffersrc or not buffersink: + with gil: + print("filtering source or sink element not found") + ret = AVERROR_UNKNOWN + self._free_filters(ret, inputs, outputs) + + if not dec_ctx.channel_layout: + dec_ctx.channel_layout = av_get_default_channel_layout(dec_ctx.channels) + + snprintf(args, sizeof(args), + "time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=0x%lx", + dec_ctx.time_base.num, dec_ctx.time_base.den, dec_ctx.sample_rate, + av_get_sample_fmt_name(dec_ctx.sample_fmt), + dec_ctx.channel_layout) + + + ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in", args, NULL, filter_graph) + if ret < 0: + with gil: + print("Cannot create audio buffer source") + self._free_filters(ret, inputs, outputs) + + ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", NULL, NULL, filter_graph) + if ret < 0: + with gil: + print("Cannot create audio buffer sink") + self._free_filters(ret, inputs, outputs) + + ret = av_opt_set_bin(buffersink_ctx, "sample_fmts", &enc_ctx.sample_fmt, sizeof(enc_ctx.sample_fmt), AV_OPT_SEARCH_CHILDREN) + if ret < 0: + with gil: + print("Cannot set output sample format") + self._free_filters(ret, inputs, outputs) + + ret = av_opt_set_bin(buffersink_ctx, "channel_layouts", &enc_ctx.channel_layout, sizeof(enc_ctx.channel_layout), AV_OPT_SEARCH_CHILDREN) + if ret < 0: + with gil: + print("Cannot set output channel layout") + self._free_filters(ret, inputs, outputs) + + ret = av_opt_set_bin(buffersink_ctx, "sample_rates", &enc_ctx.sample_rate, sizeof(enc_ctx.sample_rate), AV_OPT_SEARCH_CHILDREN) + if ret < 0: + with gil: + print("Cannot set output sample rate") + self._free_filters(ret, inputs, outputs) + + else: + ret = AVERROR_UNKNOWN + self._free_filters(ret, inputs, outputs) + + # /* Endpoints for the filter graph. */ + outputs.name = av_strdup("in") + outputs.filter_ctx = buffersrc_ctx + outputs.pad_idx = 0 + outputs.next = NULL + + inputs.name = av_strdup("out") + inputs.filter_ctx = buffersink_ctx + inputs.pad_idx = 0 + inputs.next = NULL + + if not outputs.name or not inputs.name: + ret = AVERROR(ENOMEM) + self._free_filters(ret, inputs, outputs) + + ret = avfilter_graph_parse_ptr(filter_graph, filter_spec, &inputs, &outputs, NULL) + if ret < 0: + self._free_filters(ret, inputs, outputs) + + ret = avfilter_graph_config(filter_graph, NULL) + if ret < 0: + self._free_filters(ret, inputs, outputs) + + # Fill FilteringContext + fctx.buffersrc_ctx = buffersrc_ctx + fctx.buffersink_ctx = buffersink_ctx + fctx.filter_graph = filter_graph + + self._free_filters(ret, inputs, outputs) + + cdef int encode_write_frame(self, unsigned int stream_index, int flush) nogil except 1: + cdef StreamContext *stream = &self.stream_ctx[stream_index] + cdef FilteringContext *filter = &self.filter_ctx[stream_index] + cdef AVPacket *enc_pkt = filter.enc_pkt + cdef int ret + + cdef AVFrame *filt_frame + if flush: + filt_frame = NULL + else: + filt_frame = filter.filtered_frame + + # encode filtered frame + av_packet_unref(enc_pkt) + + ret = avcodec_send_frame(stream.enc_ctx, filt_frame) + + if ret < 0: + return ret + + while ret >= 0: + ret = avcodec_receive_packet(stream.enc_ctx, enc_pkt) + + if ret == AVERROR(EAGAIN) or ret == AVERROR_EOF: + return 0 + + # prepare packet for muxing + enc_pkt.stream_index = stream_index + av_packet_rescale_ts(enc_pkt, + stream.enc_ctx.time_base, + self.ofmt_ctx.streams[stream_index].time_base) + + ret = av_interleaved_write_frame(self.ofmt_ctx, enc_pkt) + + return ret + + + cdef int filter_encode_write_frame(self, AVFrame *frame, unsigned int stream_index) nogil except 1: + cdef FilteringContext *filter = &self.filter_ctx[stream_index] + cdef int ret + ret = av_buffersrc_add_frame_flags(filter.buffersrc_ctx, frame, 0) + if ret < 0: + with gil: + print("Error while feeding the filtergraph") + return ret + + # pull filtered frames from the filtergraph + while True: + ret = av_buffersink_get_frame(filter.buffersink_ctx, filter.filtered_frame) + if ret < 0: + + # if no more frames for output - returns AVERROR(EAGAIN) + # if flushed and no more frames for output - returns AVERROR_EOF + # rewrite retcode to 0 to show it as normal procedure completion + if ret == AVERROR(EAGAIN) or ret == AVERROR_EOF: + ret = 0 + break + + filter.filtered_frame.pict_type = AV_PICTURE_TYPE_NONE + ret = self.encode_write_frame(stream_index, 0) + av_frame_unref(filter.filtered_frame) + if ret < 0: + break + + return ret + + + cdef int init_filters(self, const char *video_filters) nogil except 1: + cdef const char *filter_spec + cdef unsigned int i + cdef int ret + self.filter_ctx = av_calloc(self.ifmt_ctx.nb_streams, sizeof(FilteringContext)) + + if not self.filter_ctx: + return AVERROR(ENOMEM) + + for i in range(self.ifmt_ctx.nb_streams): + self.filter_ctx[i].buffersrc_ctx = NULL + self.filter_ctx[i].buffersink_ctx = NULL + self.filter_ctx[i].filter_graph = NULL + if not (self.ifmt_ctx.streams[i].codecpar.codec_type == AVMEDIA_TYPE_AUDIO + or self.ifmt_ctx.streams[i].codecpar.codec_type == AVMEDIA_TYPE_VIDEO): + continue + + if self.ifmt_ctx.streams[i].codecpar.codec_type == AVMEDIA_TYPE_VIDEO: + filter_spec = video_filters + + ret = self.init_filter(&self.filter_ctx[i], self.stream_ctx[i].dec_ctx, self.stream_ctx[i].enc_ctx, filter_spec) + if ret: + return ret + + self.filter_ctx[i].enc_pkt = av_packet_alloc() + if not self.filter_ctx[i].enc_pkt: + return AVERROR(ENOMEM) + + self.filter_ctx[i].filtered_frame = av_frame_alloc() + if not self.filter_ctx[i].filtered_frame: + return AVERROR(ENOMEM) + return 0 + + cdef int flush_encoder(self, unsigned int stream_index) nogil except 1: + if not (self.stream_ctx[stream_index].enc_ctx.codec.capabilities & AV_CODEC_CAP_DELAY): + return 0 + return self.encode_write_frame(stream_index, 1) + + cdef int start_transcoding(self, const char *input_file, const char *output_file, const char *video_filters, int output_width, int output_bitrate) nogil except 1: + cdef int ret + cdef AVPacket *packet = NULL + cdef unsigned int stream_index + cdef unsigned int i + + cdef StreamContext *stream + + cdef int frame_number = 0 + cdef float time_stamp = 0.0 + cdef float division_factor = 0.0 + + ret = self.open_input_file(input_file) + if ret < 0: + self._end(ret, packet) + + ret = self.open_output_file(output_file, output_width, output_bitrate) + if ret < 0: + self._end(ret, packet) + + ret = self.init_filters(video_filters) + if ret < 0: + self._end(ret, packet) + + packet = av_packet_alloc() + if not packet: + self._end(ret, packet) + + while True: + ret = av_read_frame(self.ifmt_ctx, packet) + if ret < 0: + break + + stream_index = packet.stream_index + dec_ctx = self.stream_ctx[stream_index].dec_ctx + + if not (dec_ctx.codec_type == AVMEDIA_TYPE_VIDEO or dec_ctx.codec_type == AVMEDIA_TYPE_AUDIO): + continue + + if self.filter_ctx[stream_index].filter_graph: + stream = &self.stream_ctx[stream_index] + + if dec_ctx.codec_type == AVMEDIA_TYPE_VIDEO: + division_factor = ceil((stream.dec_ctx.framerate.num / stream.dec_ctx.framerate.den / 30)) + + av_packet_rescale_ts(packet, self.ifmt_ctx.streams[stream_index].time_base, stream.dec_ctx.time_base) + ret = avcodec_send_packet(stream.dec_ctx, packet) + + if ret < 0: + with gil: + print("Decoding failed") + break + + while ret >= 0: + ret = avcodec_receive_frame(stream.dec_ctx, stream.dec_frame) + if ret == AVERROR_EOF or ret == AVERROR(EAGAIN): + break + elif ret < 0: + self._end(ret, packet) + + else: + if dec_ctx.codec_type == AVMEDIA_TYPE_VIDEO: + frame_number += 1 + # time_stamp = (frame_number * 1/2.5) + + if ( + ( + dec_ctx.codec_type == AVMEDIA_TYPE_VIDEO + and (stream.dec_ctx.framerate.num < 31 or frame_number % division_factor == 0) + ) + or dec_ctx.codec_type == AVMEDIA_TYPE_AUDIO + ): + stream.dec_frame.pts = stream.dec_frame.best_effort_timestamp + ret = self.filter_encode_write_frame(stream.dec_frame, stream_index) + if ret < 0: + self._end(ret, packet) + + else: + av_packet_rescale_ts(packet, + self.ifmt_ctx.streams[stream_index].time_base, + self.ofmt_ctx.streams[stream_index].time_base) + + ret = av_interleaved_write_frame(self.ofmt_ctx, packet) + if ret < 0: + self._end(ret, packet) + av_packet_unref(packet) + + for i in range(self.ifmt_ctx.nb_streams): + if not self.filter_ctx[i].filter_graph: + continue + + ret = self.filter_encode_write_frame(NULL, i) + if ret < 0: + with gil: + print("Flushing filter failed") + self._end(ret, packet) + + ret = self.flush_encoder(i) + if ret < 0: + with gil: + print("Flushing encoder failed") + self._end(ret, packet) + + av_write_trailer(self.ofmt_ctx) + + + cdef int _end(self, int ret, AVPacket * packet) nogil except 1: + pass + av_packet_free(&packet) + + for i in range(self.ifmt_ctx.nb_streams): + + avcodec_free_context(&self.stream_ctx[i].dec_ctx) + if self.ofmt_ctx and self.ofmt_ctx.nb_streams > i and self.ofmt_ctx.streams[i] and self.stream_ctx[i].enc_ctx: + avcodec_free_context(&self.stream_ctx[i].enc_ctx) + + if self.filter_ctx[i].filter_graph: + avfilter_graph_free(&self.filter_ctx[i].filter_graph) + + av_frame_free(&self.filter_ctx[i].filtered_frame) + av_packet_free(&self.filter_ctx[i].enc_pkt) + + av_free(self.filter_ctx) + avformat_close_input(&self.ifmt_ctx) + + if self.ofmt_ctx and not (self.ofmt_ctx.oformat.flags and AVFMT_NOFILE): + avio_closep(&self.ofmt_ctx.pb) + avformat_free_context(self.ofmt_ctx) + + if ret: + with gil: + print(f"Error occurred: {av_err2str(ret)}") + return 1 + + return 0 + + +def transcode(input_file="", output_file="", output_width=720, output_bitrate=4000): + """ + Transcode a video from one format to another with configuration modifications. + + Parameters: + + input_file(str): The file path of the input video. + + output_file(str): The file path of the output video. + + output_width (int): The width of the output video in pixels. Default is 720. + + output_bitrate (int): The bitrate of the output video in kilobits per second. Default is 4000. + + Returns: None + + """ + if not input_file or not output_file: + raise ValueError("Input file and/or output file not specified") + + output_width = int(output_width) + if output_width % 2 != 0: + output_width += 1 + + video_filters = f"scale={output_width}:-1" + + transcoder = Transcoder() + transcoder.start_transcoding( + input_file.encode('utf-8'), + output_file.encode('utf-8'), + video_filters.encode('utf-8'), + output_width, + output_bitrate + ) diff --git a/setup.py b/setup.py index 50ea72b..12c7c1a 100644 --- a/setup.py +++ b/setup.py @@ -304,7 +304,9 @@ def get_wheel_data(): mods = [ 'pic', 'threading', 'tools', 'writer', 'player/clock', 'player/core', - 'player/decoder', 'player/frame_queue', 'player/player', 'player/queue'] + 'player/decoder', 'player/frame_queue', 'player/player', 'player/queue', + 'transcoder'] + c_options['use_sdl2_mixer'] = c_options['use_sdl2_mixer']