Skip to content
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
8 changes: 8 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -576,4 +576,12 @@ pub fn build(b: *std.Build) void {
const run_mpp_decoder_test = b.addRunArtifact(mpp_decoder_test);
run_mpp_decoder_test.addArtifactArg(fake_mpp_valid);
test_step.dependOn(&run_mpp_decoder_test.step);

const bootstrap_tests = b.addTest(.{
.root_source_file = b.path("src/media/video/bootstrap_cache.zig"),
.target = b.graph.host,
.optimize = .Debug,
});
addZigImport(b, bootstrap_tests.root_module, b.graph.host, .Debug, "h264_depacketizer", "src/media/rtp/h264_depacketizer.zig");
test_step.dependOn(&b.addRunArtifact(bootstrap_tests).step);
}
129 changes: 129 additions & 0 deletions src/media/video/bootstrap_cache.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
const std = @import("std");
const builtin = @import("builtin");

pub const capacity = 4104;

pub fn read(directory: std.fs.Dir, path: []const u8, buffer: *[capacity + 1]u8) ![]const u8 {
const file = try directory.openFile(path, .{});
defer file.close();
// Zig's File.stat() uses statx, which Linux 4.9 does not provide.
const length = try file.readAll(buffer);
if (length > capacity) return error.FileTooBig;
return buffer[0..length];
}

const parameter_sets = [_]u8{ 0, 0, 0, 1, 0x67, 0x42, 0, 0, 0, 1, 0x68, 0xce };

test "cache reads preserve parameter sets" {
var directory = std.testing.tmpDir(.{});
defer directory.cleanup();
try directory.dir.writeFile(.{ .sub_path = "cache.bin", .data = &parameter_sets });
var buffer: [capacity + 1]u8 = undefined;
const data = try read(directory.dir, "cache.bin", &buffer);
try std.testing.expectEqualSlices(u8, &parameter_sets, data);

const h264 = @import("h264_depacketizer");
var depacketizer = h264.Depacketizer.init(102);
try std.testing.expect(depacketizer.setBootstrap(data));
@memset(&buffer, 0);
try std.testing.expectEqualSlices(u8, &.{ 0x67, 0x42 }, depacketizer.sps[0..depacketizer.sps_length]);
try std.testing.expectEqualSlices(u8, &.{ 0x68, 0xce }, depacketizer.pps[0..depacketizer.pps_length]);
}

test "empty cache returns no parameter sets" {
var directory = std.testing.tmpDir(.{});
defer directory.cleanup();
try directory.dir.writeFile(.{ .sub_path = "cache.bin", .data = "" });
var buffer: [capacity + 1]u8 = undefined;
try std.testing.expectEqual(@as(usize, 0), (try read(directory.dir, "cache.bin", &buffer)).len);
}

test "malformed cache does not replace active parameter sets" {
var directory = std.testing.tmpDir(.{});
defer directory.cleanup();
try directory.dir.writeFile(.{ .sub_path = "cache.bin", .data = "invalid H.264" });
var buffer: [capacity + 1]u8 = undefined;
const data = try read(directory.dir, "cache.bin", &buffer);
const h264 = @import("h264_depacketizer");
var depacketizer = h264.Depacketizer.init(102);
try std.testing.expect(depacketizer.setBootstrap(&parameter_sets));
try std.testing.expect(!depacketizer.setBootstrap(data));
try std.testing.expectEqualSlices(u8, &.{ 0x67, 0x42 }, depacketizer.sps[0..depacketizer.sps_length]);
try std.testing.expectEqualSlices(u8, &.{ 0x68, 0xce }, depacketizer.pps[0..depacketizer.pps_length]);
}

test "cache accepts the exact capacity and rejects one extra byte" {
var directory = std.testing.tmpDir(.{});
defer directory.cleanup();
var data = [_]u8{0x55} ** (capacity + 1);
@memcpy(data[0..5], &[_]u8{ 0, 0, 0, 1, 0x67 });
@memcpy(data[2052..2057], &[_]u8{ 0, 0, 0, 1, 0x68 });
try directory.dir.writeFile(.{ .sub_path = "cache.bin", .data = data[0..capacity] });
var buffer: [capacity + 1]u8 = undefined;
const loaded = try read(directory.dir, "cache.bin", &buffer);
try std.testing.expectEqualSlices(u8, data[0..capacity], loaded);
const h264 = @import("h264_depacketizer");
var depacketizer = h264.Depacketizer.init(102);
try std.testing.expect(depacketizer.setBootstrap(loaded));

try directory.dir.writeFile(.{ .sub_path = "cache.bin", .data = &data });
try std.testing.expectError(error.FileTooBig, read(directory.dir, "cache.bin", &buffer));
}

test "large cache is rejected rather than parsed as a prefix" {
var directory = std.testing.tmpDir(.{});
defer directory.cleanup();
var data = [_]u8{0x55} ** 16384;
@memcpy(data[0..parameter_sets.len], &parameter_sets);
try directory.dir.writeFile(.{ .sub_path = "cache.bin", .data = &data });
var buffer: [capacity + 1]u8 = undefined;
try std.testing.expectError(error.FileTooBig, read(directory.dir, "cache.bin", &buffer));
}

test "missing cache reports FileNotFound" {
var directory = std.testing.tmpDir(.{});
defer directory.cleanup();
var buffer: [capacity + 1]u8 = undefined;
try std.testing.expectError(error.FileNotFound, read(directory.dir, "missing.bin", &buffer));
}

test "cache read errors are returned" {
var directory = std.testing.tmpDir(.{});
defer directory.cleanup();
try directory.dir.makeDir("not-a-file");
var buffer: [capacity + 1]u8 = undefined;
try std.testing.expectError(error.IsDir, read(directory.dir, "not-a-file", &buffer));
}

test "cache loads when the kernel has no statx syscall" {
if (builtin.os.tag != .linux) return error.SkipZigTest;
var directory = std.testing.tmpDir(.{});
defer directory.cleanup();
try directory.dir.writeFile(.{ .sub_path = "cache.bin", .data = &parameter_sets });

const pid = try std.posix.fork();
if (pid == 0) {
const linux = std.os.linux;
const bpf = linux.BPF;
const Filter = extern struct { code: u16, jt: u8 = 0, jf: u8 = 0, k: u32 };
const Program = extern struct { len: u16, filter: [*]const Filter };
const filters = [_]Filter{
.{ .code = bpf.LD | bpf.W | bpf.ABS, .k = @offsetOf(linux.SECCOMP.data, "nr") },
.{ .code = bpf.JMP | bpf.JEQ | bpf.K, .jf = 1, .k = @intFromEnum(linux.SYS.statx) },
.{ .code = bpf.RET | bpf.K, .k = linux.SECCOMP.RET.ERRNO | @as(u32, @intFromEnum(linux.E.NOSYS)) },
.{ .code = bpf.RET | bpf.K, .k = linux.SECCOMP.RET.ALLOW },
};
const program = Program{ .len = filters.len, .filter = &filters };
// Keep the filter isolated from the test runner.
if (linux.prctl(@intFromEnum(linux.PR.SET_NO_NEW_PRIVS), 1, 0, 0, 0) != 0 or
linux.seccomp(linux.SECCOMP.SET_MODE_FILTER, 0, &program) != 0) linux.exit(1);
var stat: linux.Statx = undefined;
if (linux.E.init(linux.statx(-1, "", 0, 0, &stat)) != .NOSYS) linux.exit(2);
var buffer: [capacity + 1]u8 = undefined;
const data = read(directory.dir, "cache.bin", &buffer) catch linux.exit(3);
linux.exit(if (std.mem.eql(u8, &parameter_sets, data)) 0 else 4);
}
const result = std.posix.waitpid(pid, 0);
try std.testing.expect(std.posix.W.IFEXITED(result.status));
try std.testing.expectEqual(@as(u8, 0), std.posix.W.EXITSTATUS(result.status));
}
23 changes: 12 additions & 11 deletions src/media/video/video_pipeline.zig
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const std = @import("std");
const bootstrap_cache = @import("bootstrap_cache.zig");

const c = @cImport({
@cInclude("SDL2/SDL.h");
Expand All @@ -12,7 +13,7 @@ const c = @cImport({
@cInclude("video_pipeline.h");
});

const bootstrap_capacity = 4104;
const bootstrap_capacity = bootstrap_cache.capacity;
const packet_capacity = 4096;
const queue_capacity = 128;
const backpressure_attempts = 64;
Expand Down Expand Up @@ -145,21 +146,21 @@ fn zSlice(buffer: []const u8) []const u8 {
fn loadBootstrap(pipeline: *Pipeline) c_int {
const path = zSlice(&pipeline.bootstrap_path);
if (path.len == 0) return 0;
const file = std.fs.cwd().openFile(path, .{}) catch |err| {
if (err != error.FileNotFound and debugEnabled())
std.debug.print("Ignoring unreadable H.264 bootstrap cache: {s}\n", .{@errorName(err)});
var data: [bootstrap_capacity + 1]u8 = undefined;
Comment thread
alexander-clawthorne marked this conversation as resolved.
const cached = bootstrap_cache.read(std.fs.cwd(), path, &data) catch |err| {
if (debugEnabled()) switch (err) {
error.FileNotFound => {},
error.FileTooBig => std.debug.print("Ignoring oversized H.264 bootstrap cache (over {d} bytes)\n", .{bootstrap_capacity}),
else => std.debug.print("Ignoring unreadable H.264 bootstrap cache: {s}\n", .{@errorName(err)}),
};
return 0;
};
defer file.close();
const size = file.getEndPos() catch return 0;
if (size == 0 or size > bootstrap_capacity) return 0;
var data: [bootstrap_capacity]u8 = undefined;
file.reader().readNoEof(data[0..@intCast(size)]) catch return 0;
if (c.go_h264_depacketizer_set_bootstrap(pipeline.depacketizer, &data, @intCast(size)) != 0) {
if (cached.len == 0) return 0;
if (c.go_h264_depacketizer_set_bootstrap(pipeline.depacketizer, cached.ptr, cached.len) != 0) {
if (debugEnabled()) std.debug.print("Ignoring invalid H.264 bootstrap cache\n", .{});
return 0;
}
if (debugEnabled()) std.debug.print("H.264 bootstrap loaded ({d} bytes)\n", .{size});
if (debugEnabled()) std.debug.print("H.264 bootstrap loaded ({d} bytes)\n", .{cached.len});
return 0;
}

Expand Down
Loading