also looking at this
feat(video): add bounded packet history, delayed FIFO and owned snapshots
#916
3 files
+229
-0
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -22,3 +22,15 @@ zig-camera uses this module with Home JPEG/Matroska and x264/x265/fragmented MP4 | ||
| 22 | 22 | It retains compressed access units, without retaining camera DMA leases or raw |
| 23 | 23 | RGB frames. Explicit snapshots need scratch space for one largest retained |
| 24 | 24 | packet; remuxing starts with fresh codec headers and resets file timestamps. |
| 25 | ||
| 26 | For time-shifted streaming, `pushQueued` uses the same owned arena as a bounded | |
| 27 | FIFO: it never evicts or filters dependent frames, and rejects full/oversized or | |
| 28 | non-monotonic input without mutation. Use `ready(now, delay)` then `copy(0, scratch)` | |
| 29 | and `pop()` to forward matured packets; `ready(null, delay)` flushes EOF. Queue | |
| 30 | capacity must cover the delay. The caller controls keyframe starts and output | |
| 31 | errors; a full FIFO must not silently discard encoded dependencies. | |
| 32 | ||
| 33 | `cloneFrom(allocator, start)` owns a minimally sized copy of a selected suffix. | |
| 34 | Serialize it with source mutation; the resulting data can move to a worker and | |
| 35 | outlive the source. It copies wrapped bytes directly into a contiguous arena and | |
| 36 | cleans up partial allocation failures. The module creates no threads or file I/O. | |
| @@ -49,6 +49,11 @@ pub const Ring = struct { | ||
| 49 | 49 | s.skipped += 1; |
| 50 | 50 | return false; |
| 51 | 51 | } |
| 52 | s.store(packet, timestamp, key); | |
| 53 | return true; | |
| 54 | } | |
| 55 | fn store(s: *Ring, packet: []const u8, timestamp: u64, key: bool) void { | |
| 56 | s.last = timestamp; | |
| 52 | 57 | const first = @min(packet.len, s.bytes.len - s.write_pos); |
| 53 | 58 | @memcpy(s.bytes[s.write_pos..][0..first], packet[0..first]); |
| 54 | 59 | @memcpy(s.bytes[0 .. packet.len - first], packet[first..]); |
| @@ -56,7 +61,42 @@ pub const Ring = struct { | ||
| 56 | 61 | s.write_pos = if (packet.len - first != 0) packet.len - first else if (s.write_pos + first == s.bytes.len) 0 else s.write_pos + first; |
| 57 | 62 | s.count += 1; |
| 58 | 63 | s.used += packet.len; |
| 59 | return true; | |
| 64 | } | |
| 65 | /// FIFO mode: no implicit eviction or GOP filtering. A consumer must pop | |
| 66 | /// mature entries before enqueueing; overflow leaves all state unchanged. | |
| 67 | pub fn pushQueued(s: *Ring, packet: []const u8, timestamp: u64, key: bool) !void { | |
| 68 | if (packet.len == 0 or packet.len > s.bytes.len) return error.PacketTooLarge; | |
| 69 | if (s.last) |last| if (timestamp <= last) return error.InvalidTimestamp; | |
| 70 | if (s.count == s.entries.len or packet.len > s.bytes.len - s.used) return error.BufferFull; | |
| 71 | s.store(packet, timestamp, key); | |
| 72 | } | |
| 73 | pub fn pop(s: *Ring) void { | |
| 74 | if (s.count != 0) s.drop(); | |
| 75 | } | |
| 76 | /// Null clock flushes EOF; subtraction avoids overflowing timestamp+delay. | |
| 77 | pub fn ready(s: *const Ring, now: ?u64, delay: u64) bool { | |
| 78 | const e = s.entry(0) orelse return false; | |
| 79 | const clock = now orelse return true; | |
| 80 | return clock >= e.timestamp and clock - e.timestamp >= delay; | |
| 81 | } | |
| 82 | /// Own only the selected suffix. Borrowed source data never escapes. | |
| 83 | pub fn cloneFrom(s: *const Ring, a: std.mem.Allocator, start: usize) !Ring { | |
| 84 | if (start >= s.count) return error.InvalidIndex; | |
| 85 | var bytes: usize = 0; | |
| 86 | for (start..s.count) |i| bytes = try std.math.add(usize, bytes, s.entry(i).?.length); | |
| 87 | var result = try Ring.init(a, bytes, s.count - start, s.duration); | |
| 88 | errdefer result.deinit(); | |
| 89 | // Destination is contiguous in this clone; copy straight into its arena. | |
| 90 | for (start..s.count) |i| { | |
| 91 | const e = s.entry(i).?; | |
| 92 | _ = try s.copy(i, result.bytes[result.used..]); | |
| 93 | result.entries[result.count] = .{ .offset = result.used, .length = e.length, .timestamp = e.timestamp, .key = e.key }; | |
| 94 | result.count += 1; | |
| 95 | result.used += e.length; | |
| 96 | result.last = e.timestamp; | |
| 97 | } | |
| 98 | result.write_pos = 0; | |
| 99 | return result; | |
| 60 | 100 | } |
| 61 | 101 | /// Explicit owned snapshot copy; data and metadata views otherwise expire |
| 62 | 102 | /// on the next mutation. Returns the active destination slice. |
| @@ -112,3 +152,40 @@ test "initialization rejects zero limits and frees partial allocations" { | ||
| 112 | 152 | }; |
| 113 | 153 | try std.testing.checkAllAllocationFailures(std.testing.allocator, Check.run, .{}); |
| 114 | 154 | } |
| 155 | ||
| 156 | test "FIFO delay preserves dependencies and rejects overflow atomically" { | |
| 157 | var r = try Ring.init(std.testing.allocator, 8, 3, 1); | |
| 158 | defer r.deinit(); | |
| 159 | try r.pushQueued("key", 10, true); | |
| 160 | try r.pushQueued("p", 20, false); | |
| 161 | try std.testing.expect(!r.ready(19, 10)); | |
| 162 | try std.testing.expect(r.ready(20, 10)); | |
| 163 | r.pop(); | |
| 164 | try std.testing.expect(!r.entry(0).?.key); | |
| 165 | try r.pushQueued("next", 30, false); | |
| 166 | try std.testing.expectError(error.BufferFull, r.pushQueued("full", 40, true)); | |
| 167 | try std.testing.expectEqual(@as(u64, 30), r.last.?); | |
| 168 | try std.testing.expectEqual(@as(usize, 2), r.count); | |
| 169 | try std.testing.expect(r.ready(null, std.math.maxInt(u64))); | |
| 170 | try std.testing.expect(!r.ready(0, 0)); | |
| 171 | var clone = try r.cloneFrom(std.testing.allocator, 0); | |
| 172 | defer clone.deinit(); | |
| 173 | r.pop(); | |
| 174 | r.pop(); | |
| 175 | var out: [8]u8 = undefined; | |
| 176 | try std.testing.expectEqualStrings("next", try clone.copy(1, &out)); | |
| 177 | } | |
| 178 | test "suffix cloning frees partial allocations and preserves source" { | |
| 179 | const Check = struct { | |
| 180 | fn run(a: std.mem.Allocator) !void { | |
| 181 | var r = try Ring.init(a, 9, 3, 10); | |
| 182 | defer r.deinit(); | |
| 183 | _ = try r.push("first", 0, true); | |
| 184 | _ = try r.push("next", 1, true); | |
| 185 | var frozen = try r.cloneFrom(a, 1); | |
| 186 | defer frozen.deinit(); | |
| 187 | try std.testing.expectEqual(@as(usize, 2), r.count); | |
| 188 | } | |
| 189 | }; | |
| 190 | try std.testing.checkAllAllocationFailures(std.testing.allocator, Check.run, .{}); | |
| 191 | } | |