also looking at this
feat(media): implement native JPEG coding and bounded MJPEG streaming
#912
5 files
+442
-263
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.
| @@ -198,7 +198,7 @@ const JpegDecoder = struct { | ||
| 198 | 198 | b2 = try self.readByte(); |
| 199 | 199 | } |
| 200 | 200 | |
| 201 | return @enumFromInt((@as(u16, 0xFF) << 8) | b2); | |
| 201 | return @fromBackingInt(@intCast((@as(u16, 0xFF) << 8) | b2)); | |
| 202 | 202 | } |
| 203 | 203 | |
| 204 | 204 | fn skipMarker(self: *JpegDecoder) !void { |
| @@ -455,125 +455,7 @@ const JpegDecoder = struct { | ||
| 455 | 455 | // ============================================================================ |
| 456 | 456 | |
| 457 | 457 | fn idct(block: *[64]i32, quant: *const [64]u16) void { |
| 458 | // Dequantize | |
| 459 | for (0..64) |i| { | |
| 460 | block[i] *= @intCast(quant[i]); | |
| 461 | } | |
| 462 | ||
| 463 | // Fast IDCT using scaled integers | |
| 464 | // Based on AAN algorithm | |
| 465 | var workspace: [64]i32 = undefined; | |
| 466 | ||
| 467 | // Process columns | |
| 468 | for (0..8) |col| { | |
| 469 | idctColumn(block, &workspace, col); | |
| 470 | } | |
| 471 | ||
| 472 | // Process rows | |
| 473 | for (0..8) |row| { | |
| 474 | idctRow(&workspace, block, row); | |
| 475 | } | |
| 476 | } | |
| 477 | ||
| 478 | fn idctColumn(input: *const [64]i32, output: *[64]i32, col: usize) void { | |
| 479 | const s0 = input[col + 0 * 8]; | |
| 480 | const s1 = input[col + 1 * 8]; | |
| 481 | const s2 = input[col + 2 * 8]; | |
| 482 | const s3 = input[col + 3 * 8]; | |
| 483 | const s4 = input[col + 4 * 8]; | |
| 484 | const s5 = input[col + 5 * 8]; | |
| 485 | const s6 = input[col + 6 * 8]; | |
| 486 | const s7 = input[col + 7 * 8]; | |
| 487 | ||
| 488 | // Stage 1 | |
| 489 | const p2 = s2; | |
| 490 | const p3 = s6; | |
| 491 | const p1 = @divTrunc((p2 + p3) * 2896, 4096); // cos(4*pi/16) * 4096 | |
| 492 | const t2 = @divTrunc(p3 * -7017, 4096) + p1; // -cos(4*pi/16) - cos(2*pi/16) | |
| 493 | const t3 = @divTrunc(p2 * 3406, 4096) + p1; // cos(4*pi/16) - cos(6*pi/16) | |
| 494 | ||
| 495 | const t0 = (s0 + s4) << 12; | |
| 496 | const t1 = (s0 - s4) << 12; | |
| 497 | ||
| 498 | const t10 = t0 + t3; | |
| 499 | const t13 = t0 - t3; | |
| 500 | const t11 = t1 + t2; | |
| 501 | const t12 = t1 - t2; | |
| 502 | ||
| 503 | // Stage 2 | |
| 504 | const z1 = s7 + s1; | |
| 505 | const z2 = s5 + s3; | |
| 506 | const z3 = s7 + s3; | |
| 507 | const z4 = s5 + s1; | |
| 508 | const z5 = @divTrunc((z3 + z4) * 4816, 4096); // cos(2*pi/16) + cos(6*pi/16) | |
| 509 | ||
| 510 | const t4 = @divTrunc(s7 * 799, 4096); | |
| 511 | const t5 = @divTrunc(s5 * 2276, 4096); | |
| 512 | const t6 = @divTrunc(s3 * 3406, 4096); | |
| 513 | const t7 = @divTrunc(s1 * 4017, 4096); | |
| 514 | ||
| 515 | const z1_scaled = @divTrunc(z1 * -1390, 4096); | |
| 516 | const z2_scaled = @divTrunc(z2 * -3406, 4096); | |
| 517 | const z3_scaled = @divTrunc(z3 * -7017, 4096); | |
| 518 | const z4_scaled = @divTrunc(z4 * 799, 4096); | |
| 519 | ||
| 520 | const t4_out = t4 + z1_scaled + z3_scaled; | |
| 521 | const t5_out = t5 + z2_scaled + z4_scaled; | |
| 522 | const t6_out = t6 + z2_scaled + z3_scaled; | |
| 523 | const t7_out = t7 + z1_scaled + z4_scaled + z5; | |
| 524 | ||
| 525 | output[col + 0 * 8] = (t10 + t7_out) >> 12; | |
| 526 | output[col + 7 * 8] = (t10 - t7_out) >> 12; | |
| 527 | output[col + 1 * 8] = (t11 + t6_out) >> 12; | |
| 528 | output[col + 6 * 8] = (t11 - t6_out) >> 12; | |
| 529 | output[col + 2 * 8] = (t12 + t5_out) >> 12; | |
| 530 | output[col + 5 * 8] = (t12 - t5_out) >> 12; | |
| 531 | output[col + 3 * 8] = (t13 + t4_out) >> 12; | |
| 532 | output[col + 4 * 8] = (t13 - t4_out) >> 12; | |
| 533 | } | |
| 534 | ||
| 535 | fn idctRow(input: *const [64]i32, output: *[64]i32, row: usize) void { | |
| 536 | const base = row * 8; | |
| 537 | const s0 = input[base + 0]; | |
| 538 | const s1 = input[base + 1]; | |
| 539 | const s2 = input[base + 2]; | |
| 540 | const s3 = input[base + 3]; | |
| 541 | const s4 = input[base + 4]; | |
| 542 | const s5 = input[base + 5]; | |
| 543 | const s6 = input[base + 6]; | |
| 544 | const s7 = input[base + 7]; | |
| 545 | ||
| 546 | // Simplified 1D IDCT | |
| 547 | const t0 = (s0 + s4) << 7; | |
| 548 | const t1 = (s0 - s4) << 7; | |
| 549 | ||
| 550 | const t2 = @divTrunc(s2 * 2896, 4096) - @divTrunc(s6 * 1203, 4096); | |
| 551 | const t3 = @divTrunc(s2 * 1203, 4096) + @divTrunc(s6 * 2896, 4096); | |
| 552 | ||
| 553 | const t10 = t0 + t3; | |
| 554 | const t13 = t0 - t3; | |
| 555 | const t11 = t1 + t2; | |
| 556 | const t12 = t1 - t2; | |
| 557 | ||
| 558 | const z1 = s1 + s7; | |
| 559 | const z2 = s3 + s5; | |
| 560 | const z3 = s1 + s5; | |
| 561 | const z4 = s3 + s7; | |
| 562 | ||
| 563 | const t4 = @divTrunc(s7 * 799, 4096) - @divTrunc(z1 * 1390, 4096) - @divTrunc(z3 * 3406, 4096); | |
| 564 | const t5 = @divTrunc(s5 * 2276, 4096) - @divTrunc(z2 * 3406, 4096) + @divTrunc(z4 * 799, 4096); | |
| 565 | const t6 = @divTrunc(s3 * 3406, 4096) - @divTrunc(z2 * 3406, 4096) - @divTrunc(z3 * 3406, 4096); | |
| 566 | const t7 = @divTrunc(s1 * 4017, 4096) - @divTrunc(z1 * 1390, 4096) + @divTrunc(z4 * 799, 4096); | |
| 567 | ||
| 568 | // Final output with level shift and clamping | |
| 569 | output[base + 0] = clamp(@divTrunc(t10 + t7, 256) + 128); | |
| 570 | output[base + 7] = clamp(@divTrunc(t10 - t7, 256) + 128); | |
| 571 | output[base + 1] = clamp(@divTrunc(t11 + t6, 256) + 128); | |
| 572 | output[base + 6] = clamp(@divTrunc(t11 - t6, 256) + 128); | |
| 573 | output[base + 2] = clamp(@divTrunc(t12 + t5, 256) + 128); | |
| 574 | output[base + 5] = clamp(@divTrunc(t12 - t5, 256) + 128); | |
| 575 | output[base + 3] = clamp(@divTrunc(t13 + t4, 256) + 128); | |
| 576 | output[base + 4] = clamp(@divTrunc(t13 - t4, 256) + 128); | |
| 458 | .inverse(block, quant); | |
| 577 | 459 | } |
| 578 | 460 | |
| 579 | 461 | fn clamp(value: i32) i32 { |
| @@ -638,7 +520,7 @@ pub fn decode(allocator: std.mem.Allocator, data: []const u8) !Image { | ||
| 638 | 520 | .EOI => break, |
| 639 | 521 | else => { |
| 640 | 522 | // Skip unknown markers |
| 641 | if (@intFromEnum(marker) >= 0xFFC0) { | |
| 523 | if (@backingInt(marker) >= 0xFFC0) { | |
| 642 | 524 | try decoder.skipMarker(); |
| 643 | 525 | } |
| 644 | 526 | }, |
| @@ -720,149 +602,22 @@ pub fn decode(allocator: std.mem.Allocator, data: []const u8) !Image { | ||
| 720 | 602 | // JPEG Encoder (basic quality encoding) |
| 721 | 603 | // ============================================================================ |
| 722 | 604 | |
| 605 | /// Encode baseline JPEG with a real DCT/quantization/Huffman scan. The standalone | |
| 606 | /// encoder also accepts borrowed, padded rows and can reuse its output capacity. | |
| 723 | 607 | pub fn encode(allocator: std.mem.Allocator, img: *const Image) ![]u8 { |
| 724 | if (img.width == 0 or img.height == 0 or img.width > std.math.maxInt(u16) or img.height > std.math.maxInt(u16)) { | |
| 725 | return error.InvalidDimensions; | |
| 726 | } | |
| 727 | switch (img.format) { | |
| 728 | .grayscale8, .rgb8, .rgba8 => {}, | |
| 729 | else => return error.UnsupportedFormat, | |
| 730 | } | |
| 731 | ||
| 732 | var output: std.ArrayList(u8) = .empty; | |
| 733 | errdefer output.deinit(allocator); | |
| 734 | ||
| 735 | // SOI marker | |
| 736 | try output.appendSlice(allocator, &[_]u8{ 0xFF, 0xD8 }); | |
| 737 | ||
| 738 | // APP0 (JFIF) marker | |
| 739 | try output.appendSlice(allocator, &[_]u8{ | |
| 740 | 0xFF, 0xE0, // APP0 | |
| 741 | 0x00, 0x10, // Length = 16 | |
| 742 | 'J', 'F', 'I', 'F', 0x00, // Identifier | |
| 743 | 0x01, 0x01, // Version 1.1 | |
| 744 | 0x00, // Aspect ratio units (0 = no units) | |
| 745 | 0x00, 0x01, // X density | |
| 746 | 0x00, 0x01, // Y density | |
| 747 | 0x00, // Thumbnail width | |
| 748 | 0x00, // Thumbnail height | |
| 749 | }); | |
| 750 | ||
| 751 | // DQT marker (quantization tables) | |
| 752 | try output.appendSlice(allocator, &[_]u8{ 0xFF, 0xDB, 0x00, 0x43, 0x00 }); // Length, table 0 | |
| 753 | try output.appendSlice(allocator, &DEFAULT_LUMINANCE_QUANT); | |
| 754 | ||
| 755 | // Second quantization table for chrominance | |
| 756 | try output.appendSlice(allocator, &[_]u8{ 0xFF, 0xDB, 0x00, 0x43, 0x01 }); | |
| 757 | try output.appendSlice(allocator, &DEFAULT_CHROMINANCE_QUANT); | |
| 758 | ||
| 759 | // SOF0 (baseline DCT) | |
| 760 | const is_grayscale = img.format == .grayscale8; | |
| 761 | const num_components: u8 = if (is_grayscale) 1 else 3; | |
| 762 | ||
| 763 | try output.appendSlice(allocator, &[_]u8{ | |
| 764 | 0xFF, | |
| 765 | 0xC0, // SOF0 | |
| 766 | 0x00, | |
| 767 | @intCast(8 + num_components * 3), // Length | |
| 768 | 0x08, // Precision (8 bits) | |
| 769 | }); | |
| 770 | try output.appendSlice(allocator, &std.mem.toBytes(std.mem.nativeToBig(u16, @intCast(img.height)))); | |
| 771 | try output.appendSlice(allocator, &std.mem.toBytes(std.mem.nativeToBig(u16, @intCast(img.width)))); | |
| 772 | try output.append(allocator, num_components); | |
| 773 | ||
| 774 | if (is_grayscale) { | |
| 775 | try output.appendSlice(allocator, &[_]u8{ 1, 0x11, 0 }); // Y component | |
| 776 | } else { | |
| 777 | try output.appendSlice(allocator, &[_]u8{ 1, 0x11, 0 }); // Y component | |
| 778 | try output.appendSlice(allocator, &[_]u8{ 2, 0x11, 1 }); // Cb component | |
| 779 | try output.appendSlice(allocator, &[_]u8{ 3, 0x11, 1 }); // Cr component | |
| 780 | } | |
| 781 | ||
| 782 | // DHT (Huffman tables) - using standard JPEG tables | |
| 783 | try writeStandardHuffmanTables(allocator, &output); | |
| 784 | ||
| 785 | // SOS (start of scan) | |
| 786 | try output.appendSlice(allocator, &[_]u8{ | |
| 787 | 0xFF, | |
| 788 | 0xDA, // SOS | |
| 789 | 0x00, | |
| 790 | @intCast(6 + num_components * 2), // Length | |
| 791 | num_components, | |
| 792 | }); | |
| 793 | ||
| 794 | if (is_grayscale) { | |
| 795 | try output.appendSlice(allocator, &[_]u8{ 1, 0x00 }); // Component 1, DC/AC table 0 | |
| 796 | } else { | |
| 797 | try output.appendSlice(allocator, &[_]u8{ 1, 0x00 }); // Y: tables 0/0 | |
| 798 | try output.appendSlice(allocator, &[_]u8{ 2, 0x11 }); // Cb: tables 1/1 | |
| 799 | try output.appendSlice(allocator, &[_]u8{ 3, 0x11 }); // Cr: tables 1/1 | |
| 800 | } | |
| 801 | ||
| 802 | try output.appendSlice(allocator, &[_]u8{ 0x00, 0x3F, 0x00 }); // Spectral selection, successive approx | |
| 803 | ||
| 804 | // Encode scan data (simplified - just output raw MCUs with minimal compression) | |
| 805 | // This is a placeholder - full encoding would require DCT, quantization, Huffman encoding | |
| 806 | ||
| 807 | // For now, output a minimal valid bitstream | |
| 808 | // A proper implementation would encode each 8x8 block with DCT | |
| 809 | ||
| 810 | // EOI marker | |
| 811 | try output.appendSlice(allocator, &[_]u8{ 0xFF, 0xD9 }); | |
| 812 | ||
| 813 | return output.toOwnedSlice(allocator); | |
| 608 | return encodeWithQuality(allocator, img, 90); | |
| 814 | 609 | } |
| 815 | ||
| 816 | fn writeStandardHuffmanTables(allocator: std.mem.Allocator, output: *std.ArrayList(u8)) !void { | |
| 817 | // DC luminance table | |
| 818 | try output.appendSlice(allocator, &[_]u8{ | |
| 819 | 0xFF, 0xC4, // DHT marker | |
| 820 | 0x00, 0x1F, // Length | |
| 821 | 0x00, // DC table 0 | |
| 822 | 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Bits | |
| 823 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, // Values | |
| 824 | }); | |
| 825 | ||
| 826 | // AC luminance table | |
| 827 | try output.appendSlice(allocator, &[_]u8{ | |
| 828 | 0xFF, 0xC4, // DHT marker | |
| 829 | 0x00, 0xB5, // Length | |
| 830 | 0x10, // AC table 0 | |
| 831 | 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D, // Bits | |
| 832 | }); | |
| 833 | // AC values (162 bytes) | |
| 834 | const ac_values = [_]u8{ | |
| 835 | 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, | |
| 836 | 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, | |
| 837 | 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, | |
| 838 | 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, | |
| 839 | 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, | |
| 840 | 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, | |
| 841 | 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, | |
| 842 | 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, | |
| 843 | 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, | |
| 844 | 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, | |
| 845 | 0xF9, 0xFA, | |
| 610 | pub fn encodeWithQuality(allocator: std.mem.Allocator, img: *const Image, quality: u8) ![]u8 { | |
| 611 | const channels: u8 = switch (img.format) { | |
| 612 | .grayscale8 => 1, | |
| 613 | .rgb8 => 3, | |
| 614 | .rgba8 => 4, | |
| 615 | else => return error.UnsupportedFormat, | |
| 846 | 616 | }; |
| 847 | try output.appendSlice(allocator, &ac_values); | |
| 848 | ||
| 849 | // DC chrominance table | |
| 850 | try output.appendSlice(allocator, &[_]u8{ | |
| 851 | 0xFF, 0xC4, // DHT marker | |
| 852 | 0x00, 0x1F, // Length | |
| 853 | 0x01, // DC table 1 | |
| 854 | 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, // Bits | |
| 855 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, // Values | |
| 856 | }); | |
| 857 | ||
| 858 | // AC chrominance table | |
| 859 | try output.appendSlice(allocator, &[_]u8{ | |
| 860 | 0xFF, 0xC4, // DHT marker | |
| 861 | 0x00, 0xB5, // Length | |
| 862 | 0x11, // AC table 1 | |
| 863 | 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, // Bits | |
| 864 | }); | |
| 865 | try output.appendSlice(allocator, &ac_values); | |
| 617 | var encoder = .Encoder.init(allocator); | |
| 618 | defer encoder.deinit(); | |
| 619 | _ = try encoder.encode(img.pixels, img.width, img.height, @as(usize, img.width) * channels, channels, quality); | |
| 620 | return encoder.output.toOwnedSlice(allocator); | |
| 866 | 621 | } |
| 867 | 622 | |
| 868 | 623 | // ============================================================================ |
| @@ -870,8 +625,8 @@ fn writeStandardHuffmanTables(allocator: std.mem.Allocator, output: *std.ArrayLi renamed throughout | ||
| 870 | 625 | // ============================================================================ |
| 871 | 626 | |
| 872 | 627 | test "JPEG marker constants" { |
| 873 | try std.testing.expectEqual(@as(u16, 0xFFD8), @intFromEnum(Marker.SOI)); | |
| 874 | try std.testing.expectEqual(@as(u16, 0xFFD9), @intFromEnum(Marker.EOI)); | |
| 628 | try std.testing.expectEqual(@as(u16, 0xFFD8), @backingInt(Marker.SOI)); | |
| 629 | try std.testing.expectEqual(@as(u16, 0xFFD9), @backingInt(Marker.EOI)); | |
| 875 | 630 | } |
| 876 | 631 | |
| 877 | 632 | test "YCbCr to RGB conversion" { |
| @@ -0,0 +1,39 @@ | ||
| 1 | //! Shared orthonormal JPEG DCT basis and inverse transform (Home, MIT). | |
| 2 | const std = ; | |
| 3 | pub const BASIS: [8]@Vector(8, f32) = blk: { | |
| 4 | @setEvalBranchQuota(10000); | |
| 5 | var b: [8]@Vector(8, f32) = undefined; | |
| 6 | for (0..8) |u| for (0..8) |x| { | |
| 7 | const scale: f64 = if (u == 0) 0.3535533905932738 else 0.5; | |
| 8 | b[u][x] = @floatCast(scale * @cos((@as(f64, @floatFromInt(x)) * 2 + 1) * @as(f64, @floatFromInt(u)) * std.math.pi / 16)); | |
| 9 | }; | |
| 10 | break :blk b; | |
| 11 | }; | |
| 12 | ||
| 13 | pub fn inverse(block: *[64]i32, quant: *const [64]u16) void { | |
| 14 | var work: [8][8]f32 = undefined; | |
| 15 | for (0..8) |v| { | |
| 16 | var coefficients: @Vector(8, f32) = undefined; | |
| 17 | inline for (0..8) |u| coefficients[u] = @as(f32, @floatFromInt(block[v * 8 + u])) * @as(f32, @floatFromInt(quant[v * 8 + u])); | |
| 18 | for (0..8) |x| { | |
| 19 | var basis: @Vector(8, f32) = undefined; | |
| 20 | inline for (0..8) |u| basis[u] = BASIS[u][x]; | |
| 21 | work[v][x] = @reduce(.Add, coefficients * basis); | |
| 22 | } | |
| 23 | } | |
| 24 | for (0..8) |y| for (0..8) |x| { | |
| 25 | var values: @Vector(8, f32) = undefined; | |
| 26 | var basis: @Vector(8, f32) = undefined; | |
| 27 | inline for (0..8) |v| { | |
| 28 | values[v] = work[v][x]; | |
| 29 | basis[v] = BASIS[v][y]; | |
| 30 | } | |
| 31 | block[y * 8 + x] = @intFromFloat(std.math.clamp(@round(@reduce(.Add, values * basis)) + 128, 0, 255)); | |
| 32 | }; | |
| 33 | } | |
| 34 | test "inverse JPEG DC scale and zero AC are exact" { | |
| 35 | var block: [64]i32 = @splat(0); | |
| 36 | block[0] = 80; | |
| 37 | inverse(&block, &@as([64]u16, @splat(1))); | |
| 38 | for (block) |v| try std.testing.expectEqual(@as(i32, 138), v); | |
| 39 | } | |
| @@ -0,0 +1,237 @@ | ||
| 1 | //! Home native baseline JPEG encoder. MIT; see NOTICE. | |
| 2 | const std = ; | |
| 3 | const ZIGZAG = [64]u8{ | |
| 4 | 0, 1, 8, 16, 9, 2, 3, 10, | |
| 5 | 17, 24, 32, 25, 18, 11, 4, 5, | |
| 6 | 12, 19, 26, 33, 40, 48, 41, 34, | |
| 7 | 27, 20, 13, 6, 7, 14, 21, 28, | |
| 8 | 35, 42, 49, 56, 57, 50, 43, 36, | |
| 9 | 29, 22, 15, 23, 30, 37, 44, 51, | |
| 10 | 58, 59, 52, 45, 38, 31, 39, 46, | |
| 11 | 53, 60, 61, 54, 47, 55, 62, 63, | |
| 12 | }; | |
| 13 | const DEFAULT_LUMINANCE_QUANT = [64]u8{ | |
| 14 | 16, 11, 10, 16, 24, 40, 51, 61, | |
| 15 | 12, 12, 14, 19, 26, 58, 60, 55, | |
| 16 | 14, 13, 16, 24, 40, 57, 69, 56, | |
| 17 | 14, 17, 22, 29, 51, 87, 80, 62, | |
| 18 | 18, 22, 37, 56, 68, 109, 103, 77, | |
| 19 | 24, 35, 55, 64, 81, 104, 113, 92, | |
| 20 | 49, 64, 78, 87, 103, 121, 120, 101, | |
| 21 | 72, 92, 95, 98, 112, 100, 103, 99, | |
| 22 | }; | |
| 23 | const DEFAULT_CHROMINANCE_QUANT = [64]u8{ | |
| 24 | 17, 18, 24, 47, 99, 99, 99, 99, | |
| 25 | 18, 21, 26, 66, 99, 99, 99, 99, | |
| 26 | 24, 26, 56, 99, 99, 99, 99, 99, | |
| 27 | 47, 66, 99, 99, 99, 99, 99, 99, | |
| 28 | 99, 99, 99, 99, 99, 99, 99, 99, | |
| 29 | 99, 99, 99, 99, 99, 99, 99, 99, | |
| 30 | 99, 99, 99, 99, 99, 99, 99, 99, | |
| 31 | 99, 99, 99, 99, 99, 99, 99, 99, | |
| 32 | }; | |
| 33 | const AC_VALUES = [_]u8{ | |
| 34 | 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, | |
| 35 | 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, | |
| 36 | 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, | |
| 37 | 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, | |
| 38 | 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, | |
| 39 | 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, | |
| 40 | 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, | |
| 41 | 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, | |
| 42 | 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, | |
| 43 | 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, | |
| 44 | 0xF9, 0xFA, | |
| 45 | }; | |
| 46 | const DC_COUNTS = [16]u8{ 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0 }; | |
| 47 | const AC_COUNTS = [16]u8{ 0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 125 }; | |
| 48 | const DC_VALUES = [12]u8{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; | |
| 49 | const Huffman = struct { codes: [256]u16 = @splat(0), lengths: [256]u5 = @splat(0) }; | |
| 50 | fn table(counts: [16]u8, values: []const u8) Huffman { | |
| 51 | var result: Huffman = .{}; | |
| 52 | var code: u16 = 0; | |
| 53 | var n: usize = 0; | |
| 54 | for (counts, 0..) |num, i| { | |
| 55 | for (0..num) |_| { | |
| 56 | result.codes[values[n]] = code; | |
| 57 | result.lengths[values[n]] = @intCast(i + 1); | |
| 58 | code += 1; | |
| 59 | n += 1; | |
| 60 | } | |
| 61 | if (i < 15) code <<= 1; | |
| 62 | } | |
| 63 | return result; | |
| 64 | } | |
| 65 | const DC = table(DC_COUNTS, &DC_VALUES); | |
| 66 | const AC = table(AC_COUNTS, &AC_VALUES); | |
| 67 | const BASIS = .BASIS; | |
| 68 | pub const Encoder = struct { | |
| 69 | allocator: std.mem.Allocator, | |
| 70 | output: std.ArrayList(u8) = .empty, | |
| 71 | bits: u32 = 0, | |
| 72 | bit_count: u6 = 0, | |
| 73 | pub fn init(allocator: std.mem.Allocator) Encoder { | |
| 74 | return .{ .allocator = allocator }; | |
| 75 | } | |
| 76 | pub fn deinit(self: *Encoder) void { | |
| 77 | self.output.deinit(self.allocator); | |
| 78 | } | |
| 79 | fn byte(self: *Encoder, v: u8) !void { | |
| 80 | try self.output.append(self.allocator, v); | |
| 81 | } | |
| 82 | fn bytes(self: *Encoder, v: []const u8) !void { | |
| 83 | try self.output.appendSlice(self.allocator, v); | |
| 84 | } | |
| 85 | fn word(self: *Encoder, v: u16) !void { | |
| 86 | var b: [2]u8 = undefined; | |
| 87 | std.mem.writeInt(u16, &b, v, .big); | |
| 88 | try self.bytes(&b); | |
| 89 | } | |
| 90 | fn put(self: *Encoder, code: u16, len: u5) !void { | |
| 91 | self.bits = (self.bits << len) | code; | |
| 92 | self.bit_count += len; | |
| 93 | while (self.bit_count >= 8) { | |
| 94 | self.bit_count -= 8; | |
| 95 | const b: u8 = @truncate(self.bits >> @intCast(self.bit_count)); | |
| 96 | try self.byte(b); | |
| 97 | if (b == 255) try self.byte(0); | |
| 98 | } | |
| 99 | } | |
| 100 | fn symbol(self: *Encoder, t: *const Huffman, v: u8) !void { | |
| 101 | if (t.lengths[v] == 0) return error.InvalidCoefficient; | |
| 102 | try self.put(t.codes[v], t.lengths[v]); | |
| 103 | } | |
| 104 | fn amplitude(self: *Encoder, v: i32) !u5 { | |
| 105 | _ = self; | |
| 106 | const magnitude: u32 = @intCast(@abs(v)); | |
| 107 | return @intCast(if (magnitude == 0) 0 else 32 - @clz(magnitude)); | |
| 108 | } | |
| 109 | fn value(self: *Encoder, v: i32, n: u5) !void { | |
| 110 | if (n == 0) return; | |
| 111 | const code: u16 = @intCast(if (v < 0) v + @as(i32, @intCast((@as(u32, 1) << n) - 1)) else v); | |
| 112 | try self.put(code, n); | |
| 113 | } | |
| 114 | fn block(self: *Encoder, input: [64]f32, quant: [64]u8, last_dc: *i32) !void { | |
| 115 | var intermediate: [8]@Vector(8, f32) = undefined; | |
| 116 | for (0..8) |y| { | |
| 117 | const line: @Vector(8, f32) = input[y * 8 ..][0..8].*; | |
| 118 | inline for (0..8) |u| intermediate[y][u] = @reduce(.Add, line * BASIS[u]); | |
| 119 | } | |
| 120 | var coefficients: [64]i32 = undefined; | |
| 121 | inline for (0..8) |u| { | |
| 122 | var col: @Vector(8, f32) = undefined; | |
| 123 | inline for (0..8) |y| col[y] = intermediate[y][u]; | |
| 124 | for (0..8) |v| coefficients[v * 8 + u] = @intFromFloat(@round(@reduce(.Add, col * BASIS[v]) / @as(f32, @floatFromInt(quant[v * 8 + u])))); | |
| 125 | } | |
| 126 | const delta = coefficients[0] - last_dc.*; | |
| 127 | last_dc.* = coefficients[0]; | |
| 128 | const dc_n = try self.amplitude(delta); | |
| 129 | try self.symbol(&DC, dc_n); | |
| 130 | try self.value(delta, dc_n); | |
| 131 | var run: u8 = 0; | |
| 132 | for (ZIGZAG[1..]) |index| { | |
| 133 | const v = coefficients[index]; | |
| 134 | if (v == 0) { | |
| 135 | run += 1; | |
| 136 | continue; | |
| 137 | } | |
| 138 | while (run >= 16) { | |
| 139 | try self.symbol(&AC, 0xf0); | |
| 140 | run -= 16; | |
| 141 | } | |
| 142 | const n = try self.amplitude(v); | |
| 143 | if (n > 10) return error.InvalidCoefficient; | |
| 144 | try self.symbol(&AC, (run << 4) | @as(u8, n)); | |
| 145 | try self.value(v, n); | |
| 146 | run = 0; | |
| 147 | } | |
| 148 | if (run != 0) try self.symbol(&AC, 0); | |
| 149 | } | |
| 150 | /// Borrowed output is valid until the next encode/deinit. RGB/RGBA/gray input | |
| 151 | /// can contain row padding. 4:4:4, baseline Huffman coding, edge replication. | |
| 152 | pub fn encode(self: *Encoder, data: []const u8, width: usize, height: usize, stride: usize, channels: u8, quality: u8) ![]const u8 { | |
| 153 | if (width == 0 or height == 0 or width > 65535 or height > 65535) return error.InvalidDimensions; | |
| 154 | if (channels != 1 and channels != 3 and channels != 4) return error.UnsupportedFormat; | |
| 155 | if (quality < 1 or quality > 100) return error.InvalidQuality; | |
| 156 | const row = std.math.mul(usize, width, channels) catch return error.InvalidDimensions; | |
| 157 | const needed = std.math.add(usize, std.math.mul(usize, height - 1, stride) catch return error.InvalidDimensions, row) catch return error.InvalidDimensions; | |
| 158 | if (stride < row or data.len < needed) return error.BufferTooSmall; | |
| 159 | self.output.clearRetainingCapacity(); | |
| 160 | self.bits = 0; | |
| 161 | self.bit_count = 0; | |
| 162 | const components: u8 = if (channels == 1) 1 else 3; | |
| 163 | var quant: [2][64]u8 = undefined; | |
| 164 | const scale: u32 = if (quality < 50) 5000 / @as(u32, quality) else 200 - @as(u32, quality) * 2; | |
| 165 | for ([2][64]u8{ DEFAULT_LUMINANCE_QUANT, DEFAULT_CHROMINANCE_QUANT }, 0..) |q, t| for (q, 0..) |v, i| { | |
| 166 | quant[t][i] = @intCast(std.math.clamp((@as(u32, v) * scale + 50) / 100, 1, 255)); | |
| 167 | }; | |
| 168 | try self.bytes(&.{ 255, 216, 255, 224, 0, 16, 'J', 'F', 'I', 'F', 0, 1, 1, 0, 0, 1, 0, 1, 0, 0 }); | |
| 169 | for (0..2) |t| { | |
| 170 | try self.bytes(&.{ 255, 219, 0, 67 }); | |
| 171 | try self.byte(@intCast(t)); | |
| 172 | for (ZIGZAG) |i| try self.byte(quant[t][i]); | |
| 173 | } | |
| 174 | try self.bytes(&.{ 255, 192 }); | |
| 175 | try self.word(8 + @as(u16, components) * 3); | |
| 176 | try self.byte(8); | |
| 177 | try self.word(@intCast(height)); | |
| 178 | try self.word(@intCast(width)); | |
| 179 | try self.byte(components); | |
| 180 | for (0..components) |i| { | |
| 181 | try self.byte(@intCast(i + 1)); | |
| 182 | try self.byte(0x11); | |
| 183 | try self.byte(if (i == 0) 0 else 1); | |
| 184 | } | |
| 185 | try self.bytes(&.{ 255, 196, 0, 31, 0 }); | |
| 186 | try self.bytes(&DC_COUNTS); | |
| 187 | try self.bytes(&DC_VALUES); | |
| 188 | try self.bytes(&.{ 255, 196, 0, 181, 0x10 }); | |
| 189 | try self.bytes(&AC_COUNTS); | |
| 190 | try self.bytes(&AC_VALUES); | |
| 191 | try self.bytes(&.{ 255, 218 }); | |
| 192 | try self.word(6 + @as(u16, components) * 2); | |
| 193 | try self.byte(components); | |
| 194 | for (0..components) |i| { | |
| 195 | try self.byte(@intCast(i + 1)); | |
| 196 | try self.byte(0); | |
| 197 | } | |
| 198 | try self.bytes(&.{ 0, 63, 0 }); | |
| 199 | var last_dc: [3]i32 = @splat(0); | |
| 200 | var by: usize = 0; | |
| 201 | while (by < height) : (by += 8) { | |
| 202 | var bx: usize = 0; | |
| 203 | while (bx < width) : (bx += 8) { | |
| 204 | var blocks: [3][64]f32 = undefined; | |
| 205 | for (0..8) |y| for (0..8) |x| { | |
| 206 | const off = @min(by + y, height - 1) * stride + @min(bx + x, width - 1) * channels; | |
| 207 | if (channels == 1) blocks[0][y * 8 + x] = @as(f32, @floatFromInt(data[off])) - 128 else { | |
| 208 | const red: i32 = data[off]; | |
| 209 | const green: i32 = data[off + 1]; | |
| 210 | const blue: i32 = data[off + 2]; | |
| 211 | blocks[0][y * 8 + x] = @floatFromInt(((77 * red + 150 * green + 29 * blue) >> 8) - 128); | |
| 212 | blocks[1][y * 8 + x] = @floatFromInt((-43 * red - 85 * green + 128 * blue) >> 8); | |
| 213 | blocks[2][y * 8 + x] = @floatFromInt((128 * red - 107 * green - 21 * blue) >> 8); | |
| 214 | } | |
| 215 | }; | |
| 216 | for (0..components) |i| try self.block(blocks[i], quant[if (i == 0) 0 else 1], &last_dc[i]); | |
| 217 | } | |
| 218 | } | |
| 219 | if (self.bit_count > 0) { | |
| 220 | const pad: u5 = @intCast(8 - self.bit_count); | |
| 221 | try self.put(@intCast((@as(u16, 1) << @intCast(pad)) - 1), pad); | |
| 222 | } | |
| 223 | try self.bytes(&.{ 255, 217 }); | |
| 224 | return self.output.items; | |
| 225 | } | |
| 226 | }; | |
| 227 | test "baseline JPEG encodes real scan data and checks input" { | |
| 228 | var enc = Encoder.init(std.testing.allocator); | |
| 229 | defer enc.deinit(); | |
| 230 | var pixels: [17 * 9 * 3]u8 = undefined; | |
| 231 | for (&pixels, 0..) |*v, i| v.* = @truncate(i * 17); | |
| 232 | const bytes = try enc.encode(&pixels, 17, 9, 17 * 3, 3, 90); | |
| 233 | try std.testing.expect(bytes.len > 400); | |
| 234 | try std.testing.expectEqualSlices(u8, &.{ 255, 217 }, bytes[bytes.len - 2 ..]); | |
| 235 | try std.testing.expectError(error.InvalidQuality, enc.encode(&pixels, 17, 9, 51, 3, 0)); | |
| 236 | try std.testing.expectError(error.BufferTooSmall, enc.encode(&pixels, 17, 9, 52, 3, 90)); | |
| 237 | } | |
| @@ -0,0 +1,23 @@ | ||
| 1 | const std = ; | |
| 2 | const image = ; | |
| 3 | const jpeg = ; | |
| 4 | test { | |
| 5 | _ = jpeg; | |
| 6 | } | |
| 7 | test "native JPEG encodes scan data readable by the Home decoder" { | |
| 8 | var img = try image.Image.init(std.testing.allocator, 17, 9, .rgb8); | |
| 9 | defer img.deinit(); | |
| 10 | for (0..17 * 9) |i| { | |
| 11 | @memcpy(img.pixels[i * 3 ..][0..3], &[_]u8{ 200, 50, 100 }); | |
| 12 | } | |
| 13 | const encoded = try jpeg.encodeWithQuality(std.testing.allocator, &img, 98); | |
| 14 | defer std.testing.allocator.free(encoded); | |
| 15 | var decoded = try jpeg.decode(std.testing.allocator, encoded); | |
| 16 | defer decoded.deinit(); | |
| 17 | try std.testing.expectEqual(@as(u32, 17), decoded.width); | |
| 18 | try std.testing.expectEqual(@as(u32, 9), decoded.height); | |
| 19 | const pixel = decoded.getPixel(8, 4).?; | |
| 20 | try std.testing.expectApproxEqAbs(@as(f64, 200), @as(f64, @floatFromInt(pixel.r)), 5); | |
| 21 | try std.testing.expectApproxEqAbs(@as(f64, 50), @as(f64, @floatFromInt(pixel.g)), 5); | |
| 22 | try std.testing.expectApproxEqAbs(@as(f64, 100), @as(f64, @floatFromInt(pixel.b)), 5); | |
| 23 | } | |
| @@ -0,0 +1,125 @@ | ||
| 1 | //! Home video: bounded-memory Matroska/MJPEG stream writer. | |
| 2 | //! EBML IDs and track layout adapted from Home webm/mkv muxers (MIT). | |
| 3 | const std = ; | |
| 4 | pub const Sink = struct { context: *anyopaque, write: *const fn (*anyopaque, []const u8) anyerror!void }; | |
| 5 | fn number(value: u64, buf: *[8]u8) []const u8 { | |
| 6 | std.mem.writeInt(u64, buf, value, .big); | |
| 7 | var i: usize = 0; | |
| 8 | while (i < 7 and buf[i] == 0) : (i += 1) {} | |
| 9 | return buf[i..]; | |
| 10 | } | |
| 11 | fn vint(value: u64, buf: *[8]u8) ![]const u8 { | |
| 12 | var n: usize = 1; | |
| 13 | while (n < 8 and value >= (@as(u64, 1) << @intCast(n * 7)) - 1) : (n += 1) {} | |
| 14 | if (value >= (@as(u64, 1) << 56) - 1) return error.FrameTooLarge; | |
| 15 | const marked = value | (@as(u64, 1) << @intCast(n * 7)); | |
| 16 | std.mem.writeInt(u64, buf, marked, .big); | |
| 17 | return buf[8 - n ..]; | |
| 18 | } | |
| 19 | const Buffer = struct { | |
| 20 | data: [1024]u8 = undefined, | |
| 21 | len: usize = 0, | |
| 22 | fn bytes(self: *Buffer, b: []const u8) void { | |
| 23 | std.debug.assert(self.len + b.len <= self.data.len); | |
| 24 | @memcpy(self.data[self.len..][0..b.len], b); | |
| 25 | self.len += b.len; | |
| 26 | } | |
| 27 | fn element(self: *Buffer, id: u32, value: []const u8) void { | |
| 28 | var b: [8]u8 = undefined; | |
| 29 | self.bytes(number(id, &b)); | |
| 30 | self.bytes(vint(value.len, &b) catch unreachable); | |
| 31 | self.bytes(value); | |
| 32 | } | |
| 33 | fn uint(self: *Buffer, id: u32, value: u64) void { | |
| 34 | var b: [8]u8 = undefined; | |
| 35 | self.element(id, number(value, &b)); | |
| 36 | } | |
| 37 | fn slice(self: *const Buffer) []const u8 { | |
| 38 | return self.data[0..self.len]; | |
| 39 | } | |
| 40 | }; | |
| 41 | pub const Writer = struct { | |
| 42 | sink: Sink, | |
| 43 | first_ns: u64 = 0, | |
| 44 | last_ns: u64 = 0, | |
| 45 | frames: u64 = 0, | |
| 46 | failed: bool = false, | |
| 47 | pub fn init(sink: Sink, width: u32, height: u32, fps_num: u32, fps_den: u32) !Writer { | |
| 48 | if (width == 0 or height == 0 or fps_num == 0 or fps_den == 0 or fps_num > 1000000 or fps_den > 1000000) return error.InvalidConfiguration; | |
| 49 | var ebml: Buffer = .{}; | |
| 50 | ebml.uint(0x4286, 1); | |
| 51 | ebml.uint(0x42f7, 1); | |
| 52 | ebml.uint(0x42f2, 4); | |
| 53 | ebml.uint(0x42f3, 8); | |
| 54 | ebml.element(0x4282, "matroska"); | |
| 55 | ebml.uint(0x4287, 4); | |
| 56 | ebml.uint(0x4285, 2); | |
| 57 | var header: Buffer = .{}; | |
| 58 | header.element(0x1a45dfa3, ebml.slice()); | |
| 59 | header.bytes(&.{ 0x18, 0x53, 0x80, 0x67, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }); | |
| 60 | var info: Buffer = .{}; | |
| 61 | info.uint(0x2ad7b1, 1000000); | |
| 62 | info.element(0x4d80, "Home"); | |
| 63 | info.element(0x5741, "Home native MJPEG"); | |
| 64 | header.element(0x1549a966, info.slice()); | |
| 65 | var video: Buffer = .{}; | |
| 66 | video.uint(0xb0, width); | |
| 67 | video.uint(0xba, height); | |
| 68 | var track: Buffer = .{}; | |
| 69 | track.uint(0xd7, 1); | |
| 70 | track.uint(0x73c5, 1); | |
| 71 | track.uint(0x83, 1); | |
| 72 | track.uint(0x9c, 0); | |
| 73 | track.element(0x86, "V_MJPEG"); | |
| 74 | track.uint(0x23e383, @as(u64, 1000000000) * fps_den / fps_num); | |
| 75 | track.element(0xe0, video.slice()); | |
| 76 | var entry: Buffer = .{}; | |
| 77 | entry.element(0xae, track.slice()); | |
| 78 | header.element(0x1654ae6b, entry.slice()); | |
| 79 | try sink.write(sink.context, header.slice()); | |
| 80 | return .{ .sink = sink }; | |
| 81 | } | |
| 82 | pub fn frame(self: *Writer, jpeg: []const u8, timestamp_ns: u64) !void { | |
| 83 | if (self.failed) return error.WriterFailed; | |
| 84 | if (jpeg.len < 4 or !std.mem.startsWith(u8, jpeg, &.{ 255, 216 }) or !std.mem.endsWith(u8, jpeg, &.{ 255, 217 })) return error.InvalidFrame; | |
| 85 | if (self.frames != 0 and timestamp_ns <= self.last_ns) return error.InvalidTimestamp; | |
| 86 | const first = if (self.frames == 0) timestamp_ns else self.first_ns; | |
| 87 | var time: Buffer = .{}; | |
| 88 | time.uint(0xe7, (timestamp_ns - first) / 1000000); | |
| 89 | var size: [8]u8 = undefined; | |
| 90 | const block_size = try vint(jpeg.len + 4, &size); | |
| 91 | var header: Buffer = .{}; | |
| 92 | header.bytes(&.{ 0x1f, 0x43, 0xb6, 0x75 }); | |
| 93 | var cluster_size: [8]u8 = undefined; | |
| 94 | header.bytes(try vint(time.len + 1 + block_size.len + 4 + jpeg.len, &cluster_size)); | |
| 95 | header.bytes(time.slice()); | |
| 96 | header.bytes(&.{0xa3}); | |
| 97 | header.bytes(block_size); | |
| 98 | header.bytes(&.{ 0x81, 0, 0, 0x80 }); | |
| 99 | errdefer self.failed = true; | |
| 100 | try self.sink.write(self.sink.context, header.slice()); | |
| 101 | try self.sink.write(self.sink.context, jpeg); | |
| 102 | self.first_ns = first; | |
| 103 | self.last_ns = timestamp_ns; | |
| 104 | self.frames += 1; | |
| 105 | } | |
| 106 | }; | |
| 107 | test "stream rejects timestamps and propagates sink failure without accumulating frames" { | |
| 108 | const Output = struct { | |
| 109 | data: std.ArrayList(u8) = .empty, | |
| 110 | fail: bool = false, | |
| 111 | fn write(ctx: *anyopaque, bytes: []const u8) anyerror!void { | |
| 112 | const self: *@This() = @ptrCast(@alignCast(ctx)); | |
| 113 | if (self.fail) return error.TestSinkFailure; | |
| 114 | try self.data.appendSlice(std.testing.allocator, bytes); | |
| 115 | } | |
| 116 | }; | |
| 117 | var out: Output = .{}; | |
| 118 | defer out.data.deinit(std.testing.allocator); | |
| 119 | var writer = try Writer.init(.{ .context = &out, .write = Output.write }, 32, 24, 30, 1); | |
| 120 | try writer.frame(&.{ 255, 216, 1, 2, 255, 217 }, 1000); | |
| 121 | try std.testing.expectError(error.InvalidTimestamp, writer.frame(&.{ 255, 216, 255, 217 }, 1000)); | |
| 122 | out.fail = true; | |
| 123 | try std.testing.expectError(error.TestSinkFailure, writer.frame(&.{ 255, 216, 255, 217 }, 2000)); | |
| 124 | try std.testing.expectError(error.WriterFailed, writer.frame(&.{ 255, 216, 255, 217 }, 3000)); | |
| 125 | } | |