diff options
Diffstat (limited to 'src/ffmpeg.zig')
| -rw-r--r-- | src/ffmpeg.zig | 275 |
1 files changed, 275 insertions, 0 deletions
diff --git a/src/ffmpeg.zig b/src/ffmpeg.zig new file mode 100644 index 0000000..9d3bc9a --- /dev/null +++ b/src/ffmpeg.zig @@ -0,0 +1,275 @@ +const std = @import("std"); +const c = @import("c.zig"); +const gl = @import("gl.zig"); +const build_config = @import("build_config"); + +pub const Errors = error{ + AVGenericError, + AVAllocationError, + HAPBadArguments, + HAPBufferTooSmall, + HAPBadFrame, + HAPInternalError, + UnsupportedCodec, + OutOfMemory, + NoFrames, +}; + +pub fn check(err: c_int) Errors!void { + if (err >= 0) return; + var buf: [c.AV_ERROR_MAX_STRING_SIZE]u8 = undefined; + _ = c.av_make_error_string(&buf, buf.len, err); + std.debug.print("libav error: {s}\n", .{buf}); + return error.AVGenericError; +} + +pub const Decoder = struct { + num_frames: i32, + + process_packet_fn: *const fn (self: *Decoder, packet: *c.AVPacket, texture: ?*const gl.Texture) Errors!bool, + init_texture_fn: *const fn (self: *Decoder, texture: *const gl.Texture) void, + reset_fn: ?*const fn (self: *Decoder) void, + deinit_fn: *const fn (self: *Decoder, allocator: std.mem.Allocator) void, + + fn processStream( + self: *Decoder, + progress: std.Progress.Node, + format: *c.AVFormatContext, + stream: *c.AVStream, + texture: ?*const gl.Texture, + ) Errors!void { + defer progress.end(); + + try check(c.avformat_seek_file(format, stream.index, 0, 0, 0, 0)); + if (self.reset_fn) |reset_fn| reset_fn(self); + + var packet = c.av_packet_alloc(); + defer c.av_packet_free(&packet); + + self.num_frames = 0; + var res = c.av_read_frame(format, packet); + while (res >= 0) : (res = c.av_read_frame(format, packet)) { + if (packet.*.stream_index == stream.index) { + const more = try self.process_packet_fn(self, packet, texture); + progress.setCompletedItems(@intCast(self.num_frames)); + if (!more) break; + } + c.av_packet_unref(packet); + } else { + if (res != c.AVERROR_EOF) try check(res); + } + } + + pub fn createTexture( + self: *Decoder, + outer_progress: std.Progress.Node, + format: *c.AVFormatContext, + stream: *c.AVStream, + texture_type: gl.Texture.Type, + ) Errors!gl.Texture { + try self.processStream( + outer_progress.start("scanning video frames", @intCast(stream.nb_frames)), + format, + stream, + null, + ); + if (self.num_frames <= 0) return error.NoFrames; + + const texture = gl.Texture.create(texture_type); + errdefer texture.destroy(); + + self.init_texture_fn(self, &texture); + try self.processStream( + outer_progress.start("loading video frames", @intCast(self.num_frames)), + format, + stream, + &texture, + ); + return texture; + } + + pub fn deinit(self: *Decoder, allocator: std.mem.Allocator) void { + self.deinit_fn(self, allocator); + } +}; + +pub const AVDecoder = struct { + decoder: Decoder, + + sws_ctx: *c.SwsContext, + codec_par: c.AVCodecParameters, + codec_ctx: [*c]c.AVCodecContext, + + raw_frame: [*c]c.AVFrame, + rgba_frame: [*c]c.AVFrame, + + pub fn init(allocator: std.mem.Allocator, codec_par: c.AVCodecParameters) Errors!*Decoder { + const self = try allocator.create(AVDecoder); + errdefer allocator.destroy(self); + + const codec = c.avcodec_find_decoder(codec_par.codec_id) orelse return error.UnsupportedCodec; + + var codec_ctx = c.avcodec_alloc_context3(codec) orelse return error.AVAllocationError; + errdefer c.avcodec_free_context(&codec_ctx); + var raw_frame = c.av_frame_alloc() orelse return error.AVAllocationError; + errdefer c.av_frame_free(&raw_frame); + var rgba_frame = c.av_frame_alloc() orelse return error.AVAllocationError; + errdefer c.av_frame_free(&rgba_frame); + + try check(c.avcodec_parameters_to_context(codec_ctx, &codec_par)); + try check(c.avcodec_open2(codec_ctx, codec, null)); + errdefer check(c.avcodec_close(codec_ctx)) catch unreachable; + + const sws_ctx = c.sws_getContext( + codec_par.width, + codec_par.height, + codec_par.format, + codec_par.width, + codec_par.height, + c.AV_PIX_FMT_RGBA, + c.SWS_POINT, + null, + null, + 0, + ) orelse return error.AVGenericError; + errdefer c.sws_freeContext(sws_ctx); + + self.* = .{ + .decoder = .{ + .process_packet_fn = processPacket, + .init_texture_fn = initTexture, + .reset_fn = reset, + .deinit_fn = deinit, + .num_frames = 0, + }, + .sws_ctx = sws_ctx, + .codec_par = codec_par, + .codec_ctx = codec_ctx, + .raw_frame = raw_frame, + .rgba_frame = rgba_frame, + }; + return &self.decoder; + } + + fn deinit(decoder: *Decoder, allocator: std.mem.Allocator) void { + const self: *AVDecoder = @fieldParentPtr("decoder", decoder); + + const raw_ptr: [*c][*c]c.AVFrame = &self.raw_frame; + const rgba_ptr: [*c][*c]c.AVFrame = &self.rgba_frame; + c.av_frame_free(raw_ptr); + c.av_frame_free(rgba_ptr); + + c.sws_freeContext(self.sws_ctx); + check(c.avcodec_close(self.codec_ctx)) catch unreachable; + c.avcodec_free_context(&self.codec_ctx); + + allocator.destroy(self); + } + + fn reset(decoder: *Decoder) void { + const self: *AVDecoder = @fieldParentPtr("decoder", decoder); + c.avcodec_flush_buffers(self.codec_ctx); + } + + fn initTexture(decoder: *Decoder, texture: *const gl.Texture) void { + const self: *AVDecoder = @fieldParentPtr("decoder", decoder); + + texture.allocate( + self.codec_par.width, + self.codec_par.height, + self.decoder.num_frames, + c.GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, + ); + } + + fn processPacket(decoder: *Decoder, packet: *c.AVPacket, texture: ?*const gl.Texture) Errors!bool { + const self: *AVDecoder = @fieldParentPtr("decoder", decoder); + + try check(c.avcodec_send_packet(self.codec_ctx, packet)); + while (true) { + const ires = c.avcodec_receive_frame(self.codec_ctx, self.raw_frame); + if (ires == c.AVERROR_EOF) return false; + if (ires == c.AVERROR(c.EAGAIN)) return true; + try check(ires); + try check(c.sws_scale_frame(self.sws_ctx, self.rgba_frame, self.raw_frame)); + + c.glPixelStorei(c.GL_UNPACK_ROW_LENGTH, @divFloor(self.rgba_frame.*.linesize[0], @sizeOf(u8) * 4)); + + if (texture) |tex| { + tex.setLayer( + self.rgba_frame.*.width, + self.rgba_frame.*.height, + self.decoder.num_frames, + c.GL_RGBA, + self.rgba_frame.*.data[0], + ); + + if (tex.type == .TEXTURE_2D) return false; + } + + self.decoder.num_frames += 1; + } + } +}; + +pub const VideoSource = struct { + source: gl.Source, + decoder: *Decoder, + + pub fn init( + allocator: std.mem.Allocator, + progress_root: std.Progress.Node, + filename: [*:0]const u8, + texture_type: gl.Texture.Type, + ) !*gl.Source { + const self = try allocator.create(VideoSource); + errdefer allocator.destroy(self); + + const progress = progress_root.start("loading texture", 2); + defer progress.end(); + + var format_ctx: ?*c.AVFormatContext = null; + try check(c.avformat_open_input(&format_ctx, filename, null, null)); + defer c.avformat_close_input(&format_ctx); + + if (format_ctx) |format| { + try check(c.avformat_find_stream_info(format, null)); + + const video_stream = for (format.streams, 0..format.nb_streams) |c_stream, _| { + const stream = @as(*c.AVStream, c_stream orelse unreachable); + if (stream.codecpar.*.codec_type == c.AVMEDIA_TYPE_VIDEO) break stream; + } else unreachable; + + const codec_par = video_stream.codecpar.*; + + var packet = c.av_packet_alloc(); + defer c.av_packet_free(&packet); + + const is_hap = codec_par.codec_tag == c.MKTAG('H', 'a', 'p', '1') or + codec_par.codec_tag == c.MKTAG('H', 'a', 'p', '5'); + + self.* = .{ + .source = .{ + .texture = undefined, + + .update_fn = null, + .deinit_fn = deinit, + }, + .decoder = if (is_hap and build_config.have_hap) + try @import("hap.zig").HAPDecoder.init(allocator, codec_par) + else + try AVDecoder.init(allocator, codec_par), + }; + + self.source.texture = try self.decoder.createTexture(progress, format, video_stream, texture_type); + return &self.source; + } else unreachable; + } + + fn deinit(source: *const gl.Source, allocator: std.mem.Allocator) void { + const self: *const VideoSource = @fieldParentPtr("source", source); + + self.decoder.deinit(allocator); + allocator.destroy(self); + } +}; |
