also looking at this
feat(video): add native AVC/HEVC and synchronized AAC MP4 streaming
#913
14 files
+1119
-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.
| @@ -0,0 +1,34 @@ | ||
| 1 | name: Native AVC HEVC and MP4 | |
| 2 | on: | |
| 3 | push: | |
| 4 | paths: | |
| 5 | - 'packages/video/src/codecs/video/native_encoder.*' | |
| 6 | - 'packages/video/src/codecs/video/rgb_i420.zig' | |
| 7 | - 'packages/video/src/containers/mp4_stream.zig' | |
| 8 | - 'packages/video/src/native_media_tests.zig' | |
| 9 | - 'packages/video/test-native-media.sh' | |
| 10 | - '.github/workflows/native-media.yml' | |
| 11 | pull_request: | |
| 12 | paths: | |
| 13 | - 'packages/video/src/codecs/video/native_encoder.*' | |
| 14 | - 'packages/video/src/codecs/video/rgb_i420.zig' | |
| 15 | - 'packages/video/src/containers/mp4_stream.zig' | |
| 16 | - 'packages/video/src/native_media_tests.zig' | |
| 17 | - 'packages/video/test-native-media.sh' | |
| 18 | - '.github/workflows/native-media.yml' | |
| 19 | workflow_dispatch: | |
| 20 | permissions: | |
| 21 | contents: read | |
| 22 | jobs: | |
| 23 | native-media: | |
| 24 | strategy: | |
| 25 | matrix: | |
| 26 | os: [ubuntu-24.04, ubuntu-24.04-arm] | |
| 27 | runs-on: ${{ matrix.os }} | |
| 28 | steps: | |
| 29 | - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 | |
| 30 | - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 | |
| 31 | with: | |
| 32 | version: 0.15.2 | |
| 33 | - run: sudo apt-get update && sudo apt-get install -y libx264-dev libx265-dev pkg-config | |
| 34 | - run: packages/video/test-native-media.sh | |
| @@ -0,0 +1,59 @@ | ||
| 1 | # Native AVC/HEVC and streamed MP4 | |
| 2 | ||
| 3 | `src/codecs/video/native_encoder.zig` is the supported standalone Home integration | |
| 4 | for libx264/libx265. Compile `native_encoder.c` against the installed vendor | |
| 5 | headers, then link x264/x265 directly. Only Home-owned fixed scalar structs cross | |
| 6 | the C/Zig boundary; the old handwritten vendor structures in x264.zig/x265.zig | |
| 7 | are not reused. There is no FFmpeg/Libav or encoder subprocess dependency. | |
| 8 | The Zig wrapper works with Zig 0.15.2 and the Home 0.17 development toolchain. | |
| 9 | Vendor libraries keep their GPL/commercial licenses; Home modules remain MIT. | |
| 10 | ||
| 11 | The wrapper accepts checked I420 planes, returns borrowed length-prefixed NAL | |
| 12 | packets, and preserves microsecond PTS. Serialize operations per encoder; packets | |
| 13 | expire on the next call. Encode may return null while AVC VFR buffers one input. | |
| 14 | Always drain `flush()` until null before deinit. Encoding after drain or error is | |
| 15 | rejected. The integration fixes presets to veryfast, one frame thread, no B frames | |
| 16 | or lookahead, closed GOPs, AVC High / HEVC Main, 8-bit 4:2:0. Quality 1..100 maps | |
| 17 | linearly to CRF 51..0 (AVC minimum 1 to retain High-profile compatibility); bitrate >0 selects ABR in bits/sec, not strict CBR. Zero GOP | |
| 18 | means 60. HEVC uses CTU16 across instances to avoid global x265 CTU mismatches. | |
| 19 | ||
| 20 | `rgb_i420.zig` is a bounded, stride-checked conversion from RGB24. It improves | |
| 21 | Home's existing RGB conversion approach with actual 2x2 chroma averaging. Output | |
| 22 | is limited-range BT.601 with sRGB transfer / BT.709 primaries. This is SDR. | |
| 23 | ||
| 24 | `src/containers/mp4_stream.zig` provides a sink-based single-video-track fragmented | |
| 25 | MP4 writer, adapted from Home's existing MP4 box code. It writes complete AVC/HEVC | |
| 26 | sample descriptions, avcC/hvcC from encoder headers, mvex/trex, accurate moof/traf/ | |
| 27 | tfhd/trun offsets, 64-bit tfdt, and one mdat per sample. Payload writes go straight | |
| 28 | to the sink; there is no whole-recording buffer or growing sample table. | |
| 29 | ||
| 30 | The writer accepts length-prefixed samples with PTS=DTS and explicit microsecond | |
| 31 | durations. A recorder should retain one encoded packet to compute duration from | |
| 32 | the next timestamp, preserving capture gaps; use nominal duration for the last. | |
| 33 | The writer rejects malformed NAL lengths and non-increasing decode times. Partial | |
| 34 | sink failure poisons the writer. It has a fixed 16KiB header bound, no seek index, | |
| 35 | no audio, and supports the fixed 8-bit profiles emitted by this adapter. | |
| 36 | ||
| 37 | Home's older pure-Zig H.264/HEVC slice encoders and fragmented-muxer implementation | |
| 38 | remain legacy code; their feature lists are not conformance evidence for these | |
| 39 | new modules. The new modules are available through video.native_encoder, | |
| 40 | video.rgb_i420 and video.mp4_stream without pulling in the old codec graph. | |
| 41 | ||
| 42 | From the repository root on Linux: | |
| 43 | ||
| 44 | ```sh | |
| 45 | sudo apt install libx264-dev libx265-dev pkg-config | |
| 46 | packages/video/test-native-media.sh | |
| 47 | ``` | |
| 48 | ||
| 49 | Focused tests validate geometry, original timestamps, all frames through drain, | |
| 50 | invalid lifecycle, averaged chroma, fragment sizes/data offsets/64-bit time and | |
| 51 | sink failure. They pass in Debug/ReleaseSafe with Zig 0.15.2 and Home's installed | |
| 52 | 0.17 development toolchain. zig-utils/zig-camera integration additionally checks | |
| 53 | MP4 regular/gapped capture timelines and GOPs, and decodes AVC/HEVC using independent | |
| 54 | Apple AVFoundation and Linux OpenH264/libde265. These are synthetic correctness | |
| 55 | tests, not Pi hardware FPS, long-run reliability or compression-quality benchmarks. | |
| 56 | ||
| 57 | References: [x265 API](https://x265.readthedocs.io/en/master/api.html), | |
| 58 | [x264 public header](https://code.videolan.org/videolan/x264/-/blob/master/x264.h), | |
| 59 | [ISO BMFF streaming requirements](https://www.w3.org/TR/mse-byte-stream-format-isobmff/). | |
| @@ -0,0 +1,151 @@ | ||
| 1 | /* Home's header-checked x264/x265 adapter. No hand-declared vendor structures. */ | |
| 2 | #include "native_encoder.h" | |
| 3 | #include <stdlib.h> | |
| 4 | #include <string.h> | |
| 5 | #include <limits.h> | |
| 6 | #include <x264.h> | |
| 7 | #include <x265.h> | |
| 8 | struct home_encoder { | |
| 9 | uint32_t codec; | |
| 10 | x264_t *avc; | |
| 11 | x265_encoder *hevc; | |
| 12 | x265_param *param; | |
| 13 | x265_picture *in, *out; | |
| 14 | uint8_t *buffer; | |
| 15 | size_t capacity; | |
| 16 | uint32_t width, height; | |
| 17 | }; | |
| 18 | void home_encoder_close(home_encoder *e) { | |
| 19 | if (!e) return; | |
| 20 | if (e->avc) x264_encoder_close(e->avc); | |
| 21 | if (e->hevc) x265_encoder_close(e->hevc); | |
| 22 | if (e->in) x265_picture_free(e->in); | |
| 23 | if (e->out) x265_picture_free(e->out); | |
| 24 | if (e->param) x265_param_free(e->param); | |
| 25 | free(e->buffer); free(e); | |
| 26 | } | |
| 27 | int home_encoder_open(const home_encoder_config *c, home_encoder **out) { | |
| 28 | if (!out) return -1; | |
| 29 | *out=NULL; | |
| 30 | if (!c || (c->codec!=1 && c->codec!=2) || c->width<16 || c->height<16 || c->width>8192 || c->height>8192 || (c->width|c->height)&1 || !c->fps_num || !c->fps_den || c->fps_num>1000000 || c->fps_den>1000000 || c->quality<1 || c->quality>100 || c->gop>INT_MAX || (c->bitrate && c->bitrate<1000)) return -1; | |
| 31 | home_encoder *e=calloc(1,sizeof(*e)); | |
| 32 | if (!e) return -2; | |
| 33 | e->codec=c->codec; e->width=c->width; e->height=c->height; | |
| 34 | /* Quality 1..100 -> CRF 51..0; AVC clamps at 1 to keep High-profile | |
| 35 | * compatibility (CRF0 requires High444 lossless). bitrate>0 selects ABR. */ | |
| 36 | const double crf=51.0*(100-c->quality)/99.0; | |
| 37 | if (c->codec==1) { | |
| 38 | x264_param_t p; | |
| 39 | if (x264_param_default_preset(&p,"veryfast","zerolatency")<0) goto fail; | |
| 40 | p.i_width=(int)c->width; p.i_height=(int)c->height; p.i_csp=X264_CSP_I420; | |
| 41 | p.i_fps_num=c->fps_num; p.i_fps_den=c->fps_den; | |
| 42 | p.i_timebase_num=1; p.i_timebase_den=1000000; p.b_vfr_input=1; | |
| 43 | p.i_threads=1; p.i_bframe=0; p.rc.i_lookahead=0; | |
| 44 | p.i_keyint_max=c->gop ? (int)c->gop : 60; p.i_keyint_min=1; p.i_scenecut_threshold=0; | |
| 45 | p.b_open_gop=0; p.b_annexb=0; p.b_repeat_headers=0; p.i_log_level=X264_LOG_ERROR; | |
| 46 | p.vui.b_fullrange=0; p.vui.i_colorprim=1; p.vui.i_transfer=13; p.vui.i_colmatrix=6; | |
| 47 | p.rc.i_rc_method=c->bitrate ? X264_RC_ABR : X264_RC_CRF; | |
| 48 | p.rc.i_bitrate=(int)(c->bitrate/1000); p.rc.f_rf_constant=(float)(crf<1.0 ? 1.0 : crf); | |
| 49 | if (x264_param_apply_profile(&p,"high")<0) { x264_param_cleanup(&p); goto fail; } | |
| 50 | e->avc=x264_encoder_open(&p); | |
| 51 | x264_param_cleanup(&p); | |
| 52 | if (!e->avc) goto fail; | |
| 53 | } else { | |
| 54 | e->param=x265_param_alloc(); e->in=x265_picture_alloc(); e->out=x265_picture_alloc(); | |
| 55 | if (!e->param || !e->in || !e->out) goto fail; | |
| 56 | x265_param *p=e->param; | |
| 57 | if (x265_param_default_preset(p,"veryfast","zerolatency")<0) goto fail; | |
| 58 | p->sourceWidth=(int)c->width; p->sourceHeight=(int)c->height; | |
| 59 | p->fpsNum=c->fps_num; p->fpsDenom=c->fps_den; p->internalCsp=X265_CSP_I420; p->internalBitDepth=8; | |
| 60 | p->bframes=0; p->lookaheadDepth=0; p->frameNumThreads=1; | |
| 61 | p->numaPools="none"; p->bEnableWavefront=0; | |
| 62 | p->keyframeMax=c->gop ? (int)c->gop : 60; p->keyframeMin=1; p->scenecutThreshold=0; p->bOpenGOP=0; | |
| 63 | p->bAnnexB=0; p->bRepeatHeaders=0; p->bEmitInfoSEI=0; p->logLevel=X265_LOG_ERROR; | |
| 64 | /* Fixed CTU sizes across instances avoid x265's process-global CTU conflict. */ | |
| 65 | p->maxCUSize=16; p->minCUSize=8; | |
| 66 | p->vui.bEnableVideoSignalTypePresentFlag=1; p->vui.bEnableVideoFullRangeFlag=0; | |
| 67 | p->vui.bEnableColorDescriptionPresentFlag=1; p->vui.colorPrimaries=1; | |
| 68 | p->vui.transferCharacteristics=13; p->vui.matrixCoeffs=6; | |
| 69 | p->rc.rateControlMode=c->bitrate ? X265_RC_ABR : X265_RC_CRF; | |
| 70 | p->rc.bitrate=(int)(c->bitrate/1000); p->rc.rfConstant=crf; | |
| 71 | if (x265_param_apply_profile(p,"main")<0) goto fail; | |
| 72 | e->hevc=x265_encoder_open(p); | |
| 73 | if (!e->hevc) goto fail; | |
| 74 | x265_picture_init(p,e->in); x265_picture_init(p,e->out); | |
| 75 | e->in->bitDepth=8; | |
| 76 | } | |
| 77 | *out=e; return 0; | |
| 78 | fail: | |
| 79 | home_encoder_close(e); return -3; | |
| 80 | } | |
| 81 | static int collect(home_encoder *e, x264_nal_t *a, x265_nal *h, uint32_t count, home_packet *out) { | |
| 82 | size_t total=0; | |
| 83 | for (uint32_t i=0;i<count;i++) { | |
| 84 | size_t n=a ? (size_t)a[i].i_payload : h[i].sizeBytes; | |
| 85 | if (n>SIZE_MAX-total) return -2; | |
| 86 | total+=n; | |
| 87 | } | |
| 88 | if (total>e->capacity) { | |
| 89 | uint8_t *p=realloc(e->buffer,total); | |
| 90 | if (!p) return -2; | |
| 91 | e->buffer=p; e->capacity=total; | |
| 92 | } | |
| 93 | size_t offset=0; | |
| 94 | for (uint32_t i=0;i<count;i++) { | |
| 95 | size_t n=a ? (size_t)a[i].i_payload : h[i].sizeBytes; | |
| 96 | memcpy(e->buffer+offset,a ? a[i].p_payload : h[i].payload,n); offset+=n; | |
| 97 | } | |
| 98 | out->data=e->buffer; out->size=total; return 0; | |
| 99 | } | |
| 100 | int home_encoder_headers(home_encoder *e, home_packet *out) { | |
| 101 | memset(out,0,sizeof(*out)); | |
| 102 | if (e->avc) { | |
| 103 | x264_nal_t *nal=NULL; int n=0; | |
| 104 | if (x264_encoder_headers(e->avc,&nal,&n)<0 || n<=0) return -3; | |
| 105 | return collect(e,nal,NULL,(uint32_t)n,out); | |
| 106 | } | |
| 107 | x265_nal *nal=NULL; uint32_t n=0; | |
| 108 | if (x265_encoder_headers(e->hevc,&nal,&n)<0 || !n) return -3; | |
| 109 | return collect(e,NULL,nal,n,out); | |
| 110 | } | |
| 111 | int home_encoder_encode(home_encoder *e, const uint8_t *const planes[3], const int strides[3], int64_t pts, home_packet *out) { | |
| 112 | memset(out,0,sizeof(*out)); | |
| 113 | if (!planes[0] || !planes[1] || !planes[2] || strides[0]<(int)e->width || strides[1]<(int)e->width/2 || strides[2]<(int)e->width/2) return -1; | |
| 114 | if (e->avc) { | |
| 115 | x264_picture_t in, decoded; x264_picture_init(&in); x264_picture_init(&decoded); | |
| 116 | in.img.i_csp=X264_CSP_I420; in.img.i_plane=3; in.i_pts=pts; | |
| 117 | for (int i=0;i<3;i++) { in.img.plane[i]=(uint8_t *)planes[i]; in.img.i_stride[i]=strides[i]; } | |
| 118 | x264_nal_t *nal=NULL; int n=0; | |
| 119 | int ret=x264_encoder_encode(e->avc,&nal,&n,&in,&decoded); | |
| 120 | if(ret<0) return -3; | |
| 121 | if(ret==0) return 0; | |
| 122 | out->pts=decoded.i_pts; out->key=decoded.b_keyframe; | |
| 123 | return collect(e,nal,NULL,(uint32_t)n,out); | |
| 124 | } | |
| 125 | for (int i=0;i<3;i++) { e->in->planes[i]=(void *)planes[i]; e->in->stride[i]=strides[i]; } | |
| 126 | e->in->pts=pts; | |
| 127 | x265_nal *nal=NULL; uint32_t n=0; | |
| 128 | if (x265_encoder_encode(e->hevc,&nal,&n,e->in,e->out)<=0 || !n) return -3; | |
| 129 | out->pts=e->out->pts; out->key=IS_X265_TYPE_I(e->out->sliceType); | |
| 130 | return collect(e,NULL,nal,n,out); | |
| 131 | } | |
| 132 | ||
| 133 | int home_encoder_flush(home_encoder *e, home_packet *out) { | |
| 134 | memset(out,0,sizeof(*out)); | |
| 135 | if(e->avc) { | |
| 136 | if(!x264_encoder_delayed_frames(e->avc)) return 0; | |
| 137 | x264_picture_t picture; x264_picture_init(&picture); | |
| 138 | x264_nal_t *nal=NULL; int n=0; | |
| 139 | int ret=x264_encoder_encode(e->avc,&nal,&n,NULL,&picture); | |
| 140 | if(ret<0) return -3; | |
| 141 | if(ret==0) return 0; | |
| 142 | out->pts=picture.i_pts;out->key=picture.b_keyframe; | |
| 143 | return collect(e,nal,NULL,(uint32_t)n,out); | |
| 144 | } | |
| 145 | x265_nal *nal=NULL;uint32_t n=0; | |
| 146 | int ret=x265_encoder_encode(e->hevc,&nal,&n,NULL,e->out); | |
| 147 | if(ret<0) return -3; | |
| 148 | if(ret==0) return 0; | |
| 149 | out->pts=e->out->pts;out->key=IS_X265_TYPE_I(e->out->sliceType); | |
| 150 | return collect(e,NULL,nal,n,out); | |
| 151 | } | |
| @@ -0,0 +1,17 @@ | ||
| 1 | /* Home codec bridge, MIT. Compile against installed x264.h/x265.h. */ | |
| 2 | #ifndef HOME_NATIVE_ENCODER_H | |
| 3 | #define HOME_NATIVE_ENCODER_H | |
| 4 | #include <stddef.h> | |
| 5 | #include <stdint.h> | |
| 6 | typedef struct home_encoder home_encoder; | |
| 7 | typedef struct { uint32_t codec, width, height, fps_num, fps_den, bitrate, quality, gop; } home_encoder_config; | |
| 8 | /* Length-prefixed NAL units; borrowed until the next call. pts uses microseconds. | |
| 9 | * Low latency: no B frames/lookahead; AVC VFR buffers one input for duration. | |
| 10 | * Empty output means buffered. Drain flush() before close; serialize calls. */ | |
| 11 | typedef struct { const uint8_t *data; size_t size; int64_t pts; int key; } home_packet; | |
| 12 | int home_encoder_open(const home_encoder_config *, home_encoder **); | |
| 13 | int home_encoder_headers(home_encoder *, home_packet *); | |
| 14 | int home_encoder_encode(home_encoder *, const uint8_t *const planes[3], const int strides[3], int64_t pts, home_packet *); | |
| 15 | int home_encoder_flush(home_encoder *, home_packet *); | |
| 16 | void home_encoder_close(home_encoder *); | |
| 17 | #endif | |
| @@ -0,0 +1,75 @@ | ||
| 1 | //! Safe optional Home AVC/HEVC integration. Compile native_encoder.c and link | |
| 2 | //! x264/x265. Only Home's fixed scalar ABI crosses the boundary, never vendor | |
| 3 | //! structures; this module works before/after Zig's removal of @cImport. | |
| 4 | const std = ; | |
| 5 | const Handle = opaque {}; | |
| 6 | pub const Config = extern struct { codec: u32, width: u32, height: u32, fps_num: u32, fps_den: u32, bitrate: u32, quality: u32, gop: u32 }; | |
| 7 | const RawPacket = extern struct { data: [*]const u8, size: usize, pts: i64, key: c_int }; | |
| 8 | extern fn home_encoder_open(*const Config, *?*Handle) c_int; | |
| 9 | extern fn home_encoder_headers(*Handle, *RawPacket) c_int; | |
| 10 | extern fn home_encoder_encode(*Handle, *const [3][*]const u8, *const [3]c_int, i64, *RawPacket) c_int; | |
| 11 | extern fn home_encoder_flush(*Handle, *RawPacket) c_int; | |
| 12 | extern fn home_encoder_close(*Handle) void; | |
| 13 | pub const Packet = struct { data: []const u8, pts: i64, key: bool }; | |
| 14 | pub const Encoder = struct { | |
| 15 | handle: *Handle, | |
| 16 | width: u32, | |
| 17 | height: u32, | |
| 18 | last_pts: ?i64 = null, | |
| 19 | draining: bool = false, | |
| 20 | failed: bool = false, | |
| 21 | pub fn init(config: Config) !Encoder { | |
| 22 | var out: ?*Handle = null; | |
| 23 | try check(home_encoder_open(&config, &out)); | |
| 24 | return .{ .handle = out.?, .width = config.width, .height = config.height }; | |
| 25 | } | |
| 26 | pub fn deinit(self: *Encoder) void { | |
| 27 | home_encoder_close(self.handle); | |
| 28 | } | |
| 29 | pub fn headers(self: *Encoder) ![]const u8 { | |
| 30 | var packet: RawPacket = undefined; | |
| 31 | try check(home_encoder_headers(self.handle, &packet)); | |
| 32 | return packet.data[0..packet.size]; | |
| 33 | } | |
| 34 | pub fn flush(self: *Encoder) !?Packet { | |
| 35 | if (self.failed) return error.EncoderFailed; | |
| 36 | self.draining = true; | |
| 37 | var packet: RawPacket = undefined; | |
| 38 | errdefer self.failed = true; | |
| 39 | try check(home_encoder_flush(self.handle, &packet)); | |
| 40 | return view(packet); | |
| 41 | } | |
| 42 | /// Borrowed output lasts until the next call; input may be released on return. | |
| 43 | /// Null means buffered (AVC VFR retains one input); always drain flush(). | |
| 44 | pub fn encode(self: *Encoder, planes: [3][]const u8, strides: [3]u32, width: u32, height: u32, pts: i64) !?Packet { | |
| 45 | if (self.failed or self.draining) return error.EncoderFailed; | |
| 46 | if (pts < 0 or (self.last_pts != null and pts <= self.last_pts.?)) return error.InvalidTimestamp; | |
| 47 | if (width != self.width or height != self.height) return error.InvalidFrame; | |
| 48 | for (0..3) |i| { | |
| 49 | const w = if (i == 0) width else width / 2; | |
| 50 | const h = if (i == 0) height else height / 2; | |
| 51 | if (strides[i] < w or strides[i] > std.math.maxInt(c_int) or planes[i].len < @as(u64, strides[i]) * (h - 1) + w) return error.InvalidFrame; | |
| 52 | } | |
| 53 | const pointers = [3][*]const u8{ planes[0].ptr, planes[1].ptr, planes[2].ptr }; | |
| 54 | const pitch = [3]c_int{ @intCast(strides[0]), @intCast(strides[1]), @intCast(strides[2]) }; | |
| 55 | var packet: RawPacket = undefined; | |
| 56 | check(home_encoder_encode(self.handle, &pointers, &pitch, pts, &packet)) catch |err| { | |
| 57 | self.failed = true; | |
| 58 | return err; | |
| 59 | }; | |
| 60 | self.last_pts = pts; | |
| 61 | return view(packet); | |
| 62 | } | |
| 63 | }; | |
| 64 | fn check(status: c_int) !void { | |
| 65 | return switch (status) { | |
| 66 | 0 => {}, | |
| 67 | -1 => error.InvalidConfiguration, | |
| 68 | -2 => error.OutOfMemory, | |
| 69 | else => error.EncodeFailed, | |
| 70 | }; | |
| 71 | } | |
| 72 | fn view(packet: RawPacket) ?Packet { | |
| 73 | if (packet.size == 0) return null; | |
| 74 | return .{ .data = packet.data[0..packet.size], .pts = packet.pts, .key = packet.key != 0 }; | |
| 75 | } | |
| @@ -0,0 +1,37 @@ | ||
| 1 | //! Home RGB -> limited-range BT.601 I420. Adapted from perf/simd.zig, with | |
| 2 | //! checked strides and actual 2x2 chroma averaging rather than corner sampling. | |
| 3 | const std = ; | |
| 4 | pub fn convert(rgb: []const u8, width: u32, height: u32, stride: u32, out: []u8) !void { | |
| 5 | if (width == 0 or height == 0 or (width | height) & 1 != 0 or stride < @as(u64, width) * 3) return error.InvalidGeometry; | |
| 6 | const pixels = std.math.mul(usize, width, height) catch return error.InvalidGeometry; | |
| 7 | if (rgb.len < @as(u64, stride) * (height - 1) + @as(u64, width) * 3 or out.len < pixels + pixels / 2) return error.InvalidGeometry; | |
| 8 | for (0..height / 2) |cy| { | |
| 9 | for (0..width / 2) |cx| { | |
| 10 | var r: i32 = 0; | |
| 11 | var g: i32 = 0; | |
| 12 | var b: i32 = 0; | |
| 13 | for (0..2) |dy| { | |
| 14 | for (0..2) |dx| { | |
| 15 | const x = cx * 2 + dx; | |
| 16 | const y = cy * 2 + dy; | |
| 17 | const p = rgb[y * stride + x * 3 ..][0..3]; | |
| 18 | out[y * width + x] = @intCast(((66 * @as(i32, p[0]) + 129 * @as(i32, p[1]) + 25 * @as(i32, p[2]) + 128) >> 8) + 16); | |
| 19 | r += p[0]; | |
| 20 | g += p[1]; | |
| 21 | b += p[2]; | |
| 22 | } | |
| 23 | } | |
| 24 | const uv = cy * (width / 2) + cx; | |
| 25 | out[pixels + uv] = @intCast(((-38 * r - 74 * g + 112 * b + 512) >> 10) + 128); | |
| 26 | out[pixels + pixels / 4 + uv] = @intCast(((112 * r - 94 * g - 18 * b + 512) >> 10) + 128); | |
| 27 | } | |
| 28 | } | |
| 29 | } | |
| 30 | test "strided RGB conversion averages chroma and rejects truncation" { | |
| 31 | var out: [6]u8 = undefined; | |
| 32 | const rgb = [_]u8{ 255, 0, 0, 0, 255, 0, 99, 99, 0, 0, 255, 255, 255, 255 }; | |
| 33 | try convert(&rgb, 2, 2, 8, &out); | |
| 34 | try std.testing.expectEqualSlices(u8, &.{ 82, 144, 41, 235, 128, 128 }, &out); | |
| 35 | try std.testing.expectError(error.InvalidGeometry, convert(rgb[0..13], 2, 2, 8, &out)); | |
| 36 | try std.testing.expectError(error.InvalidGeometry, convert(&rgb, 1, 2, 8, &out)); | |
| 37 | } | |
| @@ -1,3 +1,5 @@ | ||
| 1 | // Legacy handwritten vendor ABI. New integrations should use native_encoder.zig, | |
| 2 | // whose C boundary is compiled against installed headers. See NATIVE_MEDIA.md. | |
| 1 | 3 | // Home Video Library - x264 FFI Integration |
| 2 | 4 | // Production H.264 encoding via libx264 |
| 3 | 5 | // https://www.videolan.org/developers/x264.html |
| @@ -1,3 +1,5 @@ | ||
| 1 | // Legacy handwritten vendor ABI. New integrations should use native_encoder.zig, | |
| 2 | // whose C boundary is compiled against installed headers. See NATIVE_MEDIA.md. | |
| 1 | 3 | // Home Video Library - x265 FFI Integration |
| 2 | 4 | // Production H.265/HEVC encoding via libx265 |
| 3 | 5 | // https://x265.readthedocs.io/ |
| @@ -0,0 +1,302 @@ | ||
| 1 | //! Home single-track streaming fragmented MP4, adapted from mp4_muxer.zig. | |
| 2 | //! Complete sample description + mvex; exact sizes/offsets; no file-sized buffer. | |
| 3 | //! AVC/HEVC are length-prefixed, with out-of-band parameter sets (avc1/hvc1). | |
| 4 | const std = ; | |
| 5 | pub const Codec = enum { h264, hevc }; | |
| 6 | pub const Sink = struct { context: *anyopaque, write: *const fn (*anyopaque, []const u8) anyerror!void }; | |
| 7 | const Buffer = struct { | |
| 8 | data: [16384]u8 = undefined, | |
| 9 | len: usize = 0, | |
| 10 | fn bytes(s: *Buffer, b: []const u8) !void { | |
| 11 | if (b.len > s.data.len - s.len) return error.HeaderTooLarge; | |
| 12 | @memcpy(s.data[s.len..][0..b.len], b); | |
| 13 | s.len += b.len; | |
| 14 | } | |
| 15 | fn zero(s: *Buffer, n: usize) !void { | |
| 16 | if (n > s.data.len - s.len) return error.HeaderTooLarge; | |
| 17 | @memset(s.data[s.len..][0..n], 0); | |
| 18 | s.len += n; | |
| 19 | } | |
| 20 | fn int16(s: *Buffer, n: u16) !void { | |
| 21 | var b: [2]u8 = undefined; | |
| 22 | std.mem.writeInt(u16, &b, n, .big); | |
| 23 | try s.bytes(&b); | |
| 24 | } | |
| 25 | fn int32(s: *Buffer, n: u32) !void { | |
| 26 | var b: [4]u8 = undefined; | |
| 27 | std.mem.writeInt(u32, &b, n, .big); | |
| 28 | try s.bytes(&b); | |
| 29 | } | |
| 30 | fn int64(s: *Buffer, n: u64) !void { | |
| 31 | var b: [8]u8 = undefined; | |
| 32 | std.mem.writeInt(u64, &b, n, .big); | |
| 33 | try s.bytes(&b); | |
| 34 | } | |
| 35 | fn box(s: *Buffer, name: *const [4]u8) !usize { | |
| 36 | const at = s.len; | |
| 37 | try s.int32(0); | |
| 38 | try s.bytes(name); | |
| 39 | return at; | |
| 40 | } | |
| 41 | fn end(s: *Buffer, at: usize) void { | |
| 42 | std.mem.writeInt(u32, s.data[at..][0..4], @intCast(s.len - at), .big); | |
| 43 | } | |
| 44 | fn full(s: *Buffer, name: *const [4]u8, flags: u32) !usize { | |
| 45 | const at = try s.box(name); | |
| 46 | try s.int32(flags); | |
| 47 | return at; | |
| 48 | } | |
| 49 | fn slice(s: *const Buffer) []const u8 { | |
| 50 | return s.data[0..s.len]; | |
| 51 | } | |
| 52 | fn matrix(s: *Buffer) !void { | |
| 53 | for ([_]u32{ 0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000 }) |v| try s.int32(v); | |
| 54 | } | |
| 55 | }; | |
| 56 | pub const Nals = struct { | |
| 57 | data: []const u8, | |
| 58 | at: usize = 0, | |
| 59 | pub fn next(s: *Nals) !?[]const u8 { | |
| 60 | if (s.at == s.data.len) return null; | |
| 61 | if (s.data.len - s.at < 4) return error.InvalidNal; | |
| 62 | const n = std.mem.readInt(u32, s.data[s.at..][0..4], .big); | |
| 63 | s.at += 4; | |
| 64 | if (n == 0 or n > s.data.len - s.at) return error.InvalidNal; | |
| 65 | const nal = s.data[s.at..][0..n]; | |
| 66 | s.at += n; | |
| 67 | return nal; | |
| 68 | } | |
| 69 | }; | |
| 70 | fn config(b: *Buffer, codec: Codec, headers: []const u8) !void { | |
| 71 | var nals: Nals = .{ .data = headers }; | |
| 72 | var sps: []const u8 = &.{}; | |
| 73 | var pps: []const u8 = &.{}; | |
| 74 | var vps: []const u8 = &.{}; | |
| 75 | while (try nals.next()) |nal| { | |
| 76 | if (nal.len > 2048) return error.HeaderTooLarge; | |
| 77 | const kind = if (codec == .h264) nal[0] & 31 else (nal[0] >> 1) & 63; | |
| 78 | if (kind == (if (codec == .h264) @as(u8, 7) else 33)) sps = nal; | |
| 79 | if (kind == (if (codec == .h264) @as(u8, 8) else 34)) pps = nal; | |
| 80 | if (codec == .hevc and kind == 32) vps = nal; | |
| 81 | } | |
| 82 | if (sps.len < 4 or pps.len < 2) return error.MissingParameterSets; | |
| 83 | const at = try b.box(if (codec == .h264) "avcC" else "hvcC"); | |
| 84 | if (codec == .h264) { | |
| 85 | // The adapter fixes AVC to 8-bit 4:2:0 High profile. | |
| 86 | if (sps[1] != 100) return error.UnsupportedProfile; | |
| 87 | try b.bytes(&.{ 1, sps[1], sps[2], sps[3], 255, 225 }); | |
| 88 | try b.int16(@intCast(sps.len)); | |
| 89 | try b.bytes(sps); | |
| 90 | try b.bytes(&.{1}); | |
| 91 | try b.int16(@intCast(pps.len)); | |
| 92 | try b.bytes(pps); | |
| 93 | try b.bytes(&.{ 253, 248, 248, 0 }); // high profile: 4:2:0, 8-bit, no SPS extensions | |
| 94 | } else { | |
| 95 | if (vps.len < 2 or sps.len < 16) return error.MissingParameterSets; | |
| 96 | // Remove emulation prevention before reading SPS profile_tier_level. | |
| 97 | var rbsp: [2048]u8 = undefined; | |
| 98 | var len: usize = 0; | |
| 99 | var zeros: usize = 0; | |
| 100 | for (sps[2..]) |byte| { | |
| 101 | if (zeros >= 2 and byte == 3) { | |
| 102 | zeros = 0; | |
| 103 | continue; | |
| 104 | } | |
| 105 | rbsp[len] = byte; | |
| 106 | len += 1; | |
| 107 | zeros = if (byte == 0) zeros + 1 else 0; | |
| 108 | } | |
| 109 | if (len < 13 or (rbsp[0] & 14) != 0 or (rbsp[1] & 31) != 1) return error.UnsupportedProfile; | |
| 110 | try b.bytes(&.{1}); | |
| 111 | try b.bytes(rbsp[1..13]); | |
| 112 | try b.bytes(&.{ 240, 0, 252, 253, 248, 248, 0, 0, 15, 3 }); | |
| 113 | for ([_]u8{ 32, 33, 34 }, [_][]const u8{ vps, sps, pps }) |kind, nal| { | |
| 114 | try b.bytes(&.{128 | kind}); | |
| 115 | try b.int16(1); | |
| 116 | try b.int16(@intCast(nal.len)); | |
| 117 | try b.bytes(nal); | |
| 118 | } | |
| 119 | } | |
| 120 | b.end(at); | |
| 121 | } | |
| 122 | pub const Writer = struct { | |
| 123 | sink: Sink, | |
| 124 | sequence: u32 = 1, | |
| 125 | last_dts: u64 = 0, | |
| 126 | frames: u64 = 0, | |
| 127 | failed: bool = false, | |
| 128 | pub fn init(sink: Sink, width: u32, height: u32, codec: Codec, headers: []const u8) !Writer { | |
| 129 | if (width == 0 or height == 0 or width > 8192 or height > 8192) return error.InvalidConfiguration; | |
| 130 | var b: Buffer = .{}; | |
| 131 | const ftyp = try b.box("ftyp"); | |
| 132 | try b.bytes("iso6"); | |
| 133 | try b.int32(512); | |
| 134 | try b.bytes("iso6isommp41"); | |
| 135 | b.end(ftyp); | |
| 136 | const moov = try b.box("moov"); | |
| 137 | const mvhd = try b.full("mvhd", 0); | |
| 138 | try b.zero(8); | |
| 139 | try b.int32(1000000); | |
| 140 | try b.int32(0); | |
| 141 | try b.int32(0x10000); | |
| 142 | try b.int16(0x100); | |
| 143 | try b.zero(10); | |
| 144 | try b.matrix(); | |
| 145 | try b.zero(24); | |
| 146 | try b.int32(2); | |
| 147 | b.end(mvhd); | |
| 148 | const trak = try b.box("trak"); | |
| 149 | const tkhd = try b.full("tkhd", 7); | |
| 150 | try b.zero(8); | |
| 151 | try b.int32(1); | |
| 152 | try b.zero(16); | |
| 153 | try b.zero(8); | |
| 154 | try b.matrix(); | |
| 155 | try b.int32(width << 16); | |
| 156 | try b.int32(height << 16); | |
| 157 | b.end(tkhd); | |
| 158 | const mdia = try b.box("mdia"); | |
| 159 | const mdhd = try b.full("mdhd", 0); | |
| 160 | try b.zero(8); | |
| 161 | try b.int32(1000000); | |
| 162 | try b.int32(0); | |
| 163 | try b.int16(0x55c4); | |
| 164 | try b.int16(0); | |
| 165 | b.end(mdhd); | |
| 166 | const hdlr = try b.full("hdlr", 0); | |
| 167 | try b.int32(0); | |
| 168 | try b.bytes("vide"); | |
| 169 | try b.zero(12); | |
| 170 | try b.bytes("Home Video\x00"); | |
| 171 | b.end(hdlr); | |
| 172 | const minf = try b.box("minf"); | |
| 173 | const vmhd = try b.full("vmhd", 1); | |
| 174 | try b.zero(8); | |
| 175 | b.end(vmhd); | |
| 176 | const dinf = try b.box("dinf"); | |
| 177 | const dref = try b.full("dref", 0); | |
| 178 | try b.int32(1); | |
| 179 | const url = try b.full("url ", 1); | |
| 180 | b.end(url); | |
| 181 | b.end(dref); | |
| 182 | b.end(dinf); | |
| 183 | const stbl = try b.box("stbl"); | |
| 184 | const stsd = try b.full("stsd", 0); | |
| 185 | try b.int32(1); | |
| 186 | const entry = try b.box(if (codec == .h264) "avc1" else "hvc1"); | |
| 187 | try b.zero(6); | |
| 188 | try b.int16(1); | |
| 189 | try b.zero(16); | |
| 190 | try b.int16(@intCast(width)); | |
| 191 | try b.int16(@intCast(height)); | |
| 192 | try b.int32(0x480000); | |
| 193 | try b.int32(0x480000); | |
| 194 | try b.int32(0); | |
| 195 | try b.int16(1); | |
| 196 | try b.zero(32); | |
| 197 | try b.int16(24); | |
| 198 | try b.int16(65535); | |
| 199 | try config(&b, codec, headers); | |
| 200 | const colr = try b.box("colr"); | |
| 201 | try b.bytes("nclx"); | |
| 202 | try b.int16(1); | |
| 203 | try b.int16(13); | |
| 204 | try b.int16(6); | |
| 205 | try b.bytes(&.{0}); | |
| 206 | b.end(colr); | |
| 207 | b.end(entry); | |
| 208 | b.end(stsd); | |
| 209 | for ([_]*const [4]u8{ "stts", "stsc", "stco" }) |name| { | |
| 210 | const at = try b.full(name, 0); | |
| 211 | try b.int32(0); | |
| 212 | b.end(at); | |
| 213 | } | |
| 214 | const stsz = try b.full("stsz", 0); | |
| 215 | try b.zero(8); | |
| 216 | b.end(stsz); | |
| 217 | b.end(stbl); | |
| 218 | b.end(minf); | |
| 219 | b.end(mdia); | |
| 220 | b.end(trak); | |
| 221 | const mvex = try b.box("mvex"); | |
| 222 | const trex = try b.full("trex", 0); | |
| 223 | try b.int32(1); | |
| 224 | try b.int32(1); | |
| 225 | try b.zero(12); | |
| 226 | b.end(trex); | |
| 227 | b.end(mvex); | |
| 228 | b.end(moov); | |
| 229 | try sink.write(sink.context, b.slice()); | |
| 230 | return .{ .sink = sink }; | |
| 231 | } | |
| 232 | /// One complete sample. Caller supplies its actual duration, using the next | |
| 233 | /// capture time; the last sample uses nominal frame duration. PTS == DTS. | |
| 234 | pub fn frame(s: *Writer, sample: []const u8, dts_us: u64, duration_us: u32, key: bool) !void { | |
| 235 | if (s.failed) return error.WriterFailed; | |
| 236 | if (sample.len == 0 or sample.len > std.math.maxInt(u32) - 8 or duration_us == 0 or (s.frames != 0 and dts_us <= s.last_dts) or s.sequence == std.math.maxInt(u32)) return error.InvalidSample; | |
| 237 | var nals: Nals = .{ .data = sample }; | |
| 238 | while (try nals.next()) |_| {} | |
| 239 | var b: Buffer = .{}; | |
| 240 | const moof = try b.box("moof"); | |
| 241 | const mfhd = try b.full("mfhd", 0); | |
| 242 | try b.int32(s.sequence); | |
| 243 | b.end(mfhd); | |
| 244 | const traf = try b.box("traf"); | |
| 245 | const tfhd = try b.full("tfhd", 0x20000); | |
| 246 | try b.int32(1); | |
| 247 | b.end(tfhd); | |
| 248 | const tfdt = try b.full("tfdt", 0x1000000); | |
| 249 | try b.int64(dts_us); | |
| 250 | b.end(tfdt); | |
| 251 | const trun = try b.full("trun", 0x701); | |
| 252 | try b.int32(1); | |
| 253 | const offset = b.len; | |
| 254 | try b.int32(0); | |
| 255 | try b.int32(duration_us); | |
| 256 | try b.int32(@intCast(sample.len)); | |
| 257 | try b.int32(if (key) 0x02000000 else 0x01010000); | |
| 258 | b.end(trun); | |
| 259 | b.end(traf); | |
| 260 | b.end(moof); | |
| 261 | std.mem.writeInt(u32, b.data[offset..][0..4], @intCast(b.len + 8), .big); | |
| 262 | try b.int32(@intCast(sample.len + 8)); | |
| 263 | try b.bytes("mdat"); | |
| 264 | errdefer s.failed = true; | |
| 265 | try s.sink.write(s.sink.context, b.slice()); | |
| 266 | try s.sink.write(s.sink.context, sample); | |
| 267 | s.sequence += 1; | |
| 268 | s.frames += 1; | |
| 269 | s.last_dts = dts_us; | |
| 270 | } | |
| 271 | }; | |
| 272 | test "NAL lengths reject truncation and empty units" { | |
| 273 | var n: Nals = .{ .data = &.{ 0, 0, 0, 2, 0x65 } }; | |
| 274 | try std.testing.expectError(error.InvalidNal, n.next()); | |
| 275 | n = .{ .data = &.{ 0, 0, 0, 0 } }; | |
| 276 | try std.testing.expectError(error.InvalidNal, n.next()); | |
| 277 | } | |
| 278 | test "fragment sizes, offsets and 64-bit time are exact; sink failure poisons writer" { | |
| 279 | const Out = struct { | |
| 280 | data: std.ArrayList(u8) = .empty, | |
| 281 | fail: bool = false, | |
| 282 | fn write(ctx: *anyopaque, bytes: []const u8) anyerror!void { | |
| 283 | const s: *@This() = @ptrCast(@alignCast(ctx)); | |
| 284 | if (s.fail) return error.DiskFull; | |
| 285 | try s.data.appendSlice(std.testing.allocator, bytes); | |
| 286 | } | |
| 287 | }; | |
| 288 | var out: Out = .{}; | |
| 289 | defer out.data.deinit(std.testing.allocator); | |
| 290 | var writer: Writer = .{ .sink = .{ .context = &out, .write = Out.write } }; | |
| 291 | try writer.frame(&.{ 0, 0, 0, 2, 0x65, 0x80 }, 0x100000000, 41667, true); | |
| 292 | const bytes = out.data.items; | |
| 293 | const size = std.mem.readInt(u32, bytes[0..4], .big); | |
| 294 | try std.testing.expectEqualStrings("moof", bytes[4..8]); | |
| 295 | try std.testing.expectEqual(@as(u32, 114), std.mem.readInt(u32, bytes[size..][0..4], .big) + size); | |
| 296 | try std.testing.expectEqual(@as(u64, 0x100000000), std.mem.readInt(u64, bytes[60..68], .big)); | |
| 297 | try std.testing.expectEqual(size + 8, std.mem.readInt(u32, bytes[84..88], .big)); | |
| 298 | try std.testing.expectError(error.InvalidSample, writer.frame(&.{ 0, 0, 0, 1, 1 }, 0x100000000, 1, false)); | |
| 299 | out.fail = true; | |
| 300 | try std.testing.expectError(error.DiskFull, writer.frame(&.{ 0, 0, 0, 1, 1 }, 0x100000001, 1, false)); | |
| 301 | try std.testing.expectError(error.WriterFailed, writer.frame(&.{ 0, 0, 0, 1, 1 }, 0x100000002, 1, false)); | |
| 302 | } | |
| @@ -0,0 +1,49 @@ | ||
| 1 | //! Focused optional codec suite, independent of Home's broad video import graph. | |
| 2 | const std = ; | |
| 3 | const native = ; | |
| 4 | test { | |
| 5 | _ = ; | |
| 6 | _ = ; | |
| 7 | } | |
| 8 | test "AVC/HEVC drain every frame with original timestamps and reject invalid lifecycle" { | |
| 9 | var y: [1024]u8 = undefined; | |
| 10 | @memset(&y, 90); | |
| 11 | var uv: [256]u8 = undefined; | |
| 12 | @memset(&uv, 128); | |
| 13 | for ([_]u32{ 1, 2 }) |codec| { | |
| 14 | var encoder = try native.Encoder.init(.{ .codec = codec, .width = 32, .height = 32, .fps_num = 24, .fps_den = 1, .quality = 90, .bitrate = 0, .gop = 4 }); | |
| 15 | defer encoder.deinit(); | |
| 16 | try std.testing.expect((try encoder.headers()).len > 8); | |
| 17 | var count: usize = 0; | |
| 18 | for (0..6) |i| { | |
| 19 | const pts: i64 = @intCast(i * 41667); | |
| 20 | if (try encoder.encode(.{ &y, &uv, &uv }, .{ 32, 16, 16 }, 32, 32, pts)) |packet| { | |
| 21 | try std.testing.expectEqual(@as(i64, @intCast(count * 41667)), packet.pts); | |
| 22 | count += 1; | |
| 23 | } | |
| 24 | try std.testing.expectError(error.InvalidTimestamp, encoder.encode(.{ &y, &uv, &uv }, .{ 32, 16, 16 }, 32, 32, pts)); | |
| 25 | } | |
| 26 | while (try encoder.flush()) |packet| { | |
| 27 | try std.testing.expectEqual(@as(i64, @intCast(count * 41667)), packet.pts); | |
| 28 | count += 1; | |
| 29 | } | |
| 30 | try std.testing.expectEqual(@as(usize, 6), count); | |
| 31 | try std.testing.expectError(error.EncoderFailed, encoder.encode(.{ &y, &uv, &uv }, .{ 32, 16, 16 }, 32, 32, 999999)); | |
| 32 | } | |
| 33 | } | |
| 34 | test "codec configuration and plane bounds fail before vendor calls" { | |
| 35 | try std.testing.expectError(error.InvalidConfiguration, native.Encoder.init(.{ .codec = 1, .width = 17, .height = 16, .fps_num = 30, .fps_den = 1, .quality = 90, .bitrate = 0, .gop = 0 })); | |
| 36 | var encoder = try native.Encoder.init(.{ .codec = 1, .width = 32, .height = 32, .fps_num = 30, .fps_den = 1, .quality = 90, .bitrate = 0, .gop = 0 }); | |
| 37 | defer encoder.deinit(); | |
| 38 | try std.testing.expectError(error.InvalidFrame, encoder.encode(.{ &.{}, &.{}, &.{} }, .{ 32, 16, 16 }, 32, 32, 0)); | |
| 39 | } | |
| 40 | test "quality endpoints preserve supported profiles" { | |
| 41 | for ([_]u32{ 1, 2 }) |codec| { | |
| 42 | for ([_]u32{ 1, 100 }) |quality| { | |
| 43 | var encoder = try native.Encoder.init(.{ .codec = codec, .width = 32, .height = 32, .fps_num = 30, .fps_den = 1, .quality = quality, .bitrate = 0, .gop = 0 }); | |
| 44 | defer encoder.deinit(); | |
| 45 | try std.testing.expect((try encoder.headers()).len > 8); | |
| 46 | try std.testing.expectEqual(@as(?native.Packet, null), try encoder.flush()); | |
| 47 | } | |
| 48 | } | |
| 49 | } | |
| @@ -97,6 +97,12 @@ pub const TrackInfo = mp4.TrackInfo; | ||
| 97 | 97 | pub const TrackType = mp4.TrackType; |
| 98 | 98 | pub const SampleTable = mp4.SampleTable; |
| 99 | 99 | |
| 100 | // Optional header-checked AVC/HEVC integration: compile native_encoder.c and | |
| 101 | // link x264/x265 only when this module is used. See NATIVE_MEDIA.md. | |
| 102 | pub const native_encoder = ; | |
| 103 | pub const rgb_i420 = ; | |
| 104 | pub const mp4_stream = ; | |
| 105 | ||
| 100 | 106 | pub const mp4_muxer = ; |
| 101 | 107 | pub const Mp4Muxer = mp4_muxer.Mp4Muxer; |
| 102 | 108 | pub const VideoTrackConfig = mp4_muxer.VideoTrackConfig; |
| @@ -0,0 +1,11 @@ | ||
| 1 | #!/bin/sh | |
| 2 | # Focused suite for the optional Home AVC/HEVC + MP4 modules. No FFmpeg. | |
| 3 | set -eu | |
| 4 | cd "$(dirname "$0")" | |
| 5 | build_dir=$(mktemp -d "${TMPDIR:-/tmp}/home-native-media.XXXXXX") | |
| 6 | trap 'rm -rf "$build_dir"' EXIT HUP INT TERM | |
| 7 | cc -std=c11 -Wall -Wextra -Werror $(pkg-config --cflags x264 x265) -c src/codecs/video/native_encoder.c -o "$build_dir/encoder.o" | |
| 8 | ${ZIG:-zig} fmt --check src/codecs/video/native_encoder.zig src/codecs/video/rgb_i420.zig src/containers/mp4_stream.zig src/native_media_tests.zig | |
| 9 | for mode in Debug ReleaseSafe; do | |
| 10 | ${ZIG:-zig} test src/native_media_tests.zig -O "$mode" -lc "$build_dir/encoder.o" $(pkg-config --libs x264 x265) ${HOME_MEDIA_LDFLAGS:-} | |
| 11 | done | |