also looking at this
feat(video): add native AVC/HEVC and synchronized AAC MP4 streaming
#913
14 files
+1119
-0
| @@ -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,89 @@ | ||
| 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/). | |
| 60 | ||
| 61 | ## AAC-LC with synchronized fragmented MP4 | |
| 62 | ||
| 63 | `mp4_stream.Writer.initAv` extends the same bounded video muxer with an optional | |
| 64 | AAC track. Its `AudioConfig` consumes the encoder's real AudioSpecificConfig, | |
| 65 | rate/channels/bitrate and codec delay. Use the native encoder in | |
| 66 | [Home #918](https://github.com/home-lang/home/pull/918); the muxer itself has no | |
| 67 | codec dependency and remains independently testable. | |
| 68 | ||
| 69 | Audio raw access units use the sample-rate timebase and `mp4a`/`esds`; video | |
| 70 | retains microsecond capture timestamps. Pass a sink with both write and patch | |
| 71 | callbacks. One AAC packet is staged so the final duration can omit padding. | |
| 72 | After draining AAC, `finish(start_us, input_frames)` emits the final packet and | |
| 73 | patches only fixed header fields: 64-bit movie/track/media durations and edit | |
| 74 | lengths. Memory does not grow with recording duration. A seekable sink and clean | |
| 75 | finalization are required; pipe output and crash recovery are not provided. | |
| 76 | ||
| 77 | The audio edit list represents the initial offset and actual encoder delay. | |
| 78 | Global `sgpd` and per-fragment `sbgp` roll associations mark explicit decoder | |
| 79 | pre-roll. Without those associations Apple readers can apply an extra implicit | |
| 80 | priming trim. The final short fragment duration prevents padded tail playback. | |
| 81 | Both are checked by FAAD2/AVFoundation camera integration, including sub-frame | |
| 82 | input, 44.1/48kHz, stereo, video gaps and independently decodable split files. | |
| 83 | Do not infer AAC time from callback scheduling or assume a fixed codec delay. | |
| 84 | The caller must provide continuous PCM to the encoder and preserve sample counts. | |
| 85 | Microsecond movie/edit times can round by less than one PCM sample. | |
| 86 | ||
| 87 | Four focused muxer tests pass on Zig 0.15.2 and the Home development toolchain, | |
| 88 | including sink failure poisoning and fixed edit/duration patch values. No | |
| 89 | FFmpeg/Libav or complete Home media-graph validation is represented here. | |
| @@ -0,0 +1,25 @@ | ||
| 1 | # Header-checked native video adapter | |
| 2 | ||
| 3 | `codecs/video/native_encoder.{c,h,zig}` compiles the x264/x265 boundary against | |
| 4 | installed vendor headers. Only Home-owned scalar configuration and borrowed | |
| 5 | length-prefixed packet data cross the interface. The Zig wrapper validates input | |
| 6 | geometry, timestamps and encode/drain state. Link the C source and libx264/libx265. | |
| 7 | ||
| 8 | `Encoder.requestKeyframe()` / `home_encoder_request_keyframe` requests an IDR for | |
| 9 | the next submitted input. Requests coalesce. This supports independently decodable | |
| 10 | recording splits without waiting for the regular GOP interval. Inspect emitted | |
| 11 | packet timestamps and key flags before switching outputs: buffered AVC output | |
| 12 | can precede the newly requested IDR. Keep headers outside packet history and | |
| 13 | remux each segment with a fresh sample description and a rebased timeline. | |
| 14 | ||
| 15 | Use requests only on an active encoder before draining, serialize every operation, | |
| 16 | and consume borrowed output before another adapter call. The Zig wrapper rejects | |
| 17 | requests after drain/failure. A request neither drains nor resets the encoder. | |
| 18 | ||
| 19 | The standalone `tests/native_encoder_idr.zig` imports this module as `encoder` and | |
| 20 | links its compiled C boundary. Its regression encodes eight frames under GOP100, | |
| 21 | forces IDRs at inputs 3 and 6, checks actual AVC/HEVC IDR NAL types and packet keys, | |
| 22 | counts every delayed/flushed frame, and rejects requests after drain. It passes | |
| 23 | on Zig 0.15.2 and Home's installed 0.17 development toolchain with x264/x265. | |
| 24 | zig-camera additionally remuxes split files and pre-roll snapshots and validates | |
| 25 | them with independent decoders. No FFmpeg or Libav is used in this path. | |
| @@ -0,0 +1,157 @@ | ||
| 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 | int force_key; | |
| 18 | }; | |
| 19 | void home_encoder_close(home_encoder *e) { | |
| 20 | if (!e) return; | |
| 21 | if (e->avc) x264_encoder_close(e->avc); | |
| 22 | if (e->hevc) x265_encoder_close(e->hevc); | |
| 23 | if (e->in) x265_picture_free(e->in); | |
| 24 | if (e->out) x265_picture_free(e->out); | |
| 25 | if (e->param) x265_param_free(e->param); | |
| 26 | free(e->buffer); free(e); | |
| 27 | } | |
| 28 | int home_encoder_open(const home_encoder_config *c, home_encoder **out) { | |
| 29 | if (!out) return -1; | |
| 30 | *out=NULL; | |
| 31 | 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; | |
| 32 | home_encoder *e=calloc(1,sizeof(*e)); | |
| 33 | if (!e) return -2; | |
| 34 | e->codec=c->codec; e->width=c->width; e->height=c->height; | |
| 35 | /* Quality 1..100 -> CRF 51..0; AVC clamps at 1 to keep High-profile | |
| 36 | * compatibility (CRF0 requires High444 lossless). bitrate>0 selects ABR. */ | |
| 37 | const double crf=51.0*(100-c->quality)/99.0; | |
| 38 | if (c->codec==1) { | |
| 39 | x264_param_t p; | |
| 40 | if (x264_param_default_preset(&p,"veryfast","zerolatency")<0) goto fail; | |
| 41 | p.i_width=(int)c->width; p.i_height=(int)c->height; p.i_csp=X264_CSP_I420; | |
| 42 | p.i_fps_num=c->fps_num; p.i_fps_den=c->fps_den; | |
| 43 | p.i_timebase_num=1; p.i_timebase_den=1000000; p.b_vfr_input=1; | |
| 44 | p.i_threads=1; p.i_bframe=0; p.rc.i_lookahead=0; | |
| 45 | p.i_keyint_max=c->gop ? (int)c->gop : 60; p.i_keyint_min=1; p.i_scenecut_threshold=0; | |
| 46 | p.b_open_gop=0; p.b_annexb=0; p.b_repeat_headers=0; p.i_log_level=X264_LOG_ERROR; | |
| 47 | p.vui.b_fullrange=0; p.vui.i_colorprim=1; p.vui.i_transfer=13; p.vui.i_colmatrix=6; | |
| 48 | p.rc.i_rc_method=c->bitrate ? X264_RC_ABR : X264_RC_CRF; | |
| 49 | p.rc.i_bitrate=(int)(c->bitrate/1000); p.rc.f_rf_constant=(float)(crf<1.0 ? 1.0 : crf); | |
| 50 | if (x264_param_apply_profile(&p,"high")<0) { x264_param_cleanup(&p); goto fail; } | |
| 51 | e->avc=x264_encoder_open(&p); | |
| 52 | x264_param_cleanup(&p); | |
| 53 | if (!e->avc) goto fail; | |
| 54 | } else { | |
| 55 | e->param=x265_param_alloc(); e->in=x265_picture_alloc(); e->out=x265_picture_alloc(); | |
| 56 | if (!e->param || !e->in || !e->out) goto fail; | |
| 57 | x265_param *p=e->param; | |
| 58 | if (x265_param_default_preset(p,"veryfast","zerolatency")<0) goto fail; | |
| 59 | p->sourceWidth=(int)c->width; p->sourceHeight=(int)c->height; | |
| 60 | p->fpsNum=c->fps_num; p->fpsDenom=c->fps_den; p->internalCsp=X265_CSP_I420; p->internalBitDepth=8; | |
| 61 | p->bframes=0; p->lookaheadDepth=0; p->frameNumThreads=1; | |
| 62 | p->numaPools="none"; p->bEnableWavefront=0; | |
| 63 | p->keyframeMax=c->gop ? (int)c->gop : 60; p->keyframeMin=1; p->scenecutThreshold=0; p->bOpenGOP=0; | |
| 64 | p->bAnnexB=0; p->bRepeatHeaders=0; p->bEmitInfoSEI=0; p->logLevel=X265_LOG_ERROR; | |
| 65 | /* Fixed CTU sizes across instances avoid x265's process-global CTU conflict. */ | |
| 66 | p->maxCUSize=16; p->minCUSize=8; | |
| 67 | p->vui.bEnableVideoSignalTypePresentFlag=1; p->vui.bEnableVideoFullRangeFlag=0; | |
| 68 | p->vui.bEnableColorDescriptionPresentFlag=1; p->vui.colorPrimaries=1; | |
| 69 | p->vui.transferCharacteristics=13; p->vui.matrixCoeffs=6; | |
| 70 | p->rc.rateControlMode=c->bitrate ? X265_RC_ABR : X265_RC_CRF; | |
| 71 | p->rc.bitrate=(int)(c->bitrate/1000); p->rc.rfConstant=crf; | |
| 72 | if (x265_param_apply_profile(p,"main")<0) goto fail; | |
| 73 | e->hevc=x265_encoder_open(p); | |
| 74 | if (!e->hevc) goto fail; | |
| 75 | x265_picture_init(p,e->in); x265_picture_init(p,e->out); | |
| 76 | e->in->bitDepth=8; | |
| 77 | } | |
| 78 | *out=e; return 0; | |
| 79 | fail: | |
| 80 | home_encoder_close(e); return -3; | |
| 81 | } | |
| 82 | static int collect(home_encoder *e, x264_nal_t *a, x265_nal *h, uint32_t count, home_packet *out) { | |
| 83 | size_t total=0; | |
| 84 | for (uint32_t i=0;i<count;i++) { | |
| 85 | size_t n=a ? (size_t)a[i].i_payload : h[i].sizeBytes; | |
| 86 | if (n>SIZE_MAX-total) return -2; | |
| 87 | total+=n; | |
| 88 | } | |
| 89 | if (total>e->capacity) { | |
| 90 | uint8_t *p=realloc(e->buffer,total); | |
| 91 | if (!p) return -2; | |
| 92 | e->buffer=p; e->capacity=total; | |
| 93 | } | |
| 94 | size_t offset=0; | |
| 95 | for (uint32_t i=0;i<count;i++) { | |
| 96 | size_t n=a ? (size_t)a[i].i_payload : h[i].sizeBytes; | |
| 97 | memcpy(e->buffer+offset,a ? a[i].p_payload : h[i].payload,n); offset+=n; | |
| 98 | } | |
| 99 | out->data=e->buffer; out->size=total; return 0; | |
| 100 | } | |
| 101 | int home_encoder_headers(home_encoder *e, home_packet *out) { | |
| 102 | memset(out,0,sizeof(*out)); | |
| 103 | if (e->avc) { | |
| 104 | x264_nal_t *nal=NULL; int n=0; | |
| 105 | if (x264_encoder_headers(e->avc,&nal,&n)<0 || n<=0) return -3; | |
| 106 | return collect(e,nal,NULL,(uint32_t)n,out); | |
| 107 | } | |
| 108 | x265_nal *nal=NULL; uint32_t n=0; | |
| 109 | if (x265_encoder_headers(e->hevc,&nal,&n)<0 || !n) return -3; | |
| 110 | return collect(e,NULL,nal,n,out); | |
| 111 | } | |
| 112 | int home_encoder_encode(home_encoder *e, const uint8_t *const planes[3], const int strides[3], int64_t pts, home_packet *out) { | |
| 113 | memset(out,0,sizeof(*out)); | |
| 114 | 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; | |
| 115 | if (e->avc) { | |
| 116 | x264_picture_t in, decoded; x264_picture_init(&in); x264_picture_init(&decoded); | |
| 117 | in.i_type=e->force_key ? X264_TYPE_IDR : X264_TYPE_AUTO; | |
| 118 | e->force_key=0; | |
| 119 | in.img.i_csp=X264_CSP_I420; in.img.i_plane=3; in.i_pts=pts; | |
| 120 | for (int i=0;i<3;i++) { in.img.plane[i]=(uint8_t *)planes[i]; in.img.i_stride[i]=strides[i]; } | |
| 121 | x264_nal_t *nal=NULL; int n=0; | |
| 122 | int ret=x264_encoder_encode(e->avc,&nal,&n,&in,&decoded); | |
| 123 | if(ret<0) return -3; | |
| 124 | if(ret==0) return 0; | |
| 125 | out->pts=decoded.i_pts; out->key=decoded.b_keyframe; | |
| 126 | return collect(e,nal,NULL,(uint32_t)n,out); | |
| 127 | } | |
| 128 | for (int i=0;i<3;i++) { e->in->planes[i]=(void *)planes[i]; e->in->stride[i]=strides[i]; } | |
| 129 | e->in->pts=pts; | |
| 130 | e->in->sliceType=e->force_key ? X265_TYPE_IDR : X265_TYPE_AUTO;e->force_key=0; | |
| 131 | x265_nal *nal=NULL; uint32_t n=0; | |
| 132 | if (x265_encoder_encode(e->hevc,&nal,&n,e->in,e->out)<=0 || !n) return -3; | |
| 133 | out->pts=e->out->pts; out->key=IS_X265_TYPE_I(e->out->sliceType); | |
| 134 | return collect(e,NULL,nal,n,out); | |
| 135 | } | |
| 136 | ||
| 137 | int home_encoder_flush(home_encoder *e, home_packet *out) { | |
| 138 | memset(out,0,sizeof(*out)); | |
| 139 | if(e->avc) { | |
| 140 | if(!x264_encoder_delayed_frames(e->avc)) return 0; | |
| 141 | x264_picture_t picture; x264_picture_init(&picture); | |
| 142 | x264_nal_t *nal=NULL; int n=0; | |
| 143 | int ret=x264_encoder_encode(e->avc,&nal,&n,NULL,&picture); | |
| 144 | if(ret<0) return -3; | |
| 145 | if(ret==0) return 0; | |
| 146 | out->pts=picture.i_pts;out->key=picture.b_keyframe; | |
| 147 | return collect(e,nal,NULL,(uint32_t)n,out); | |
| 148 | } | |
| 149 | x265_nal *nal=NULL;uint32_t n=0; | |
| 150 | int ret=x265_encoder_encode(e->hevc,&nal,&n,NULL,e->out); | |
| 151 | if(ret<0) return -3; | |
| 152 | if(ret==0) return 0; | |
| 153 | out->pts=e->out->pts;out->key=IS_X265_TYPE_I(e->out->sliceType); | |
| 154 | return collect(e,NULL,nal,n,out); | |
| 155 | } | |
| 156 | ||
| 157 | int home_encoder_request_keyframe(home_encoder *e) { if(!e)return -1;e->force_key=1;return 0; } | |
| @@ -0,0 +1,18 @@ | ||
| 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_request_keyframe(home_encoder *); | |
| 16 | int home_encoder_flush(home_encoder *, home_packet *); | |
| 17 | void home_encoder_close(home_encoder *); | |
| 18 | #endif | |
| @@ -0,0 +1,80 @@ | ||
| 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_request_keyframe(*Handle) c_int; | |
| 12 | extern fn home_encoder_flush(*Handle, *RawPacket) c_int; | |
| 13 | extern fn home_encoder_close(*Handle) void; | |
| 14 | pub const Packet = struct { data: []const u8, pts: i64, key: bool }; | |
| 15 | pub const Encoder = struct { | |
| 16 | handle: *Handle, | |
| 17 | width: u32, | |
| 18 | height: u32, | |
| 19 | last_pts: ?i64 = null, | |
| 20 | draining: bool = false, | |
| 21 | failed: bool = false, | |
| 22 | pub fn init(config: Config) !Encoder { | |
| 23 | var out: ?*Handle = null; | |
| 24 | try check(home_encoder_open(&config, &out)); | |
| 25 | return .{ .handle = out.?, .width = config.width, .height = config.height }; | |
| 26 | } | |
| 27 | pub fn deinit(self: *Encoder) void { | |
| 28 | home_encoder_close(self.handle); | |
| 29 | } | |
| 30 | pub fn headers(self: *Encoder) ![]const u8 { | |
| 31 | var packet: RawPacket = undefined; | |
| 32 | try check(home_encoder_headers(self.handle, &packet)); | |
| 33 | return packet.data[0..packet.size]; | |
| 34 | } | |
| 35 | pub fn requestKeyframe(self: *Encoder) !void { | |
| 36 | if (self.failed or self.draining) return error.EncoderFailed; | |
| 37 | try check(home_encoder_request_keyframe(self.handle)); | |
| 38 | } | |
| 39 | pub fn flush(self: *Encoder) !?Packet { | |
| 40 | if (self.failed) return error.EncoderFailed; | |
| 41 | self.draining = true; | |
| 42 | var packet: RawPacket = undefined; | |
| 43 | errdefer self.failed = true; | |
| 44 | try check(home_encoder_flush(self.handle, &packet)); | |
| 45 | return view(packet); | |
| 46 | } | |
| 47 | /// Borrowed output lasts until the next call; input may be released on return. | |
| 48 | /// Null means buffered (AVC VFR retains one input); always drain flush(). | |
| 49 | pub fn encode(self: *Encoder, planes: [3][]const u8, strides: [3]u32, width: u32, height: u32, pts: i64) !?Packet { | |
| 50 | if (self.failed or self.draining) return error.EncoderFailed; | |
| 51 | if (pts < 0 or (self.last_pts != null and pts <= self.last_pts.?)) return error.InvalidTimestamp; | |
| 52 | if (width != self.width or height != self.height) return error.InvalidFrame; | |
| 53 | for (0..3) |i| { | |
| 54 | const w = if (i == 0) width else width / 2; | |
| 55 | const h = if (i == 0) height else height / 2; | |
| 56 | 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; | |
| 57 | } | |
| 58 | const pointers = [3][*]const u8{ planes[0].ptr, planes[1].ptr, planes[2].ptr }; | |
| 59 | const pitch = [3]c_int{ @intCast(strides[0]), @intCast(strides[1]), @intCast(strides[2]) }; | |
| 60 | var packet: RawPacket = undefined; | |
| 61 | check(home_encoder_encode(self.handle, &pointers, &pitch, pts, &packet)) catch |err| { | |
| 62 | self.failed = true; | |
| 63 | return err; | |
| 64 | }; | |
| 65 | self.last_pts = pts; | |
| 66 | return view(packet); | |
| 67 | } | |
| 68 | }; | |
| 69 | fn check(status: c_int) !void { | |
| 70 | return switch (status) { | |
| 71 | 0 => {}, | |
| 72 | -1 => error.InvalidConfiguration, | |
| 73 | -2 => error.OutOfMemory, | |
| 74 | else => error.EncodeFailed, | |
| 75 | }; | |
| 76 | } | |
| 77 | fn view(packet: RawPacket) ?Packet { | |
| 78 | if (packet.size == 0) return null; | |
| 79 | return .{ .data = packet.data[0..packet.size], .pts = packet.pts, .key = packet.key != 0 }; | |
| 80 | } | |
| @@ -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/ |
packages/video/src/containers/mp4_stream.zigadded+571-0
| @@ -0,0 +1,571 @@ | ||
| 1 | //! Home streaming fragmented MP4 with optional AAC-LC audio, 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 { | |
| 7 | context: *anyopaque, | |
| 8 | write: *const fn (*anyopaque, []const u8) anyerror!void, | |
| 9 | patch: ?*const fn (*anyopaque, u64, []const u8) anyerror!void = null, | |
| 10 | }; | |
| 11 | pub const AudioConfig = struct { sample_rate: u32, channels: u32, bitrate: u32, delay: u32, config: [2]u8 }; | |
| 12 | const AudioOffsets = struct { track: usize = 0, media: usize = 0, empty: usize = 0, edit: usize = 0 }; | |
| 13 | ||
| 14 | const Buffer = struct { | |
| 15 | data: [16384]u8 = undefined, | |
| 16 | len: usize = 0, | |
| 17 | fn bytes(s: *Buffer, b: []const u8) !void { | |
| 18 | if (b.len > s.data.len - s.len) return error.HeaderTooLarge; | |
| 19 | @memcpy(s.data[s.len..][0..b.len], b); | |
| 20 | s.len += b.len; | |
| 21 | } | |
| 22 | fn zero(s: *Buffer, n: usize) !void { | |
| 23 | if (n > s.data.len - s.len) return error.HeaderTooLarge; | |
| 24 | @memset(s.data[s.len..][0..n], 0); | |
| 25 | s.len += n; | |
| 26 | } | |
| 27 | fn int16(s: *Buffer, n: u16) !void { | |
| 28 | var b: [2]u8 = undefined; | |
| 29 | std.mem.writeInt(u16, &b, n, .big); | |
| 30 | try s.bytes(&b); | |
| 31 | } | |
| 32 | fn int32(s: *Buffer, n: u32) !void { | |
| 33 | var b: [4]u8 = undefined; | |
| 34 | std.mem.writeInt(u32, &b, n, .big); | |
| 35 | try s.bytes(&b); | |
| 36 | } | |
| 37 | fn int64(s: *Buffer, n: u64) !void { | |
| 38 | var b: [8]u8 = undefined; | |
| 39 | std.mem.writeInt(u64, &b, n, .big); | |
| 40 | try s.bytes(&b); | |
| 41 | } | |
| 42 | fn box(s: *Buffer, name: *const [4]u8) !usize { | |
| 43 | const at = s.len; | |
| 44 | try s.int32(0); | |
| 45 | try s.bytes(name); | |
| 46 | return at; | |
| 47 | } | |
| 48 | fn end(s: *Buffer, at: usize) void { | |
| 49 | std.mem.writeInt(u32, s.data[at..][0..4], @intCast(s.len - at), .big); | |
| 50 | } | |
| 51 | fn full(s: *Buffer, name: *const [4]u8, flags: u32) !usize { | |
| 52 | const at = try s.box(name); | |
| 53 | try s.int32(flags); | |
| 54 | return at; | |
| 55 | } | |
| 56 | fn slice(s: *const Buffer) []const u8 { | |
| 57 | return s.data[0..s.len]; | |
| 58 | } | |
| 59 | fn matrix(s: *Buffer) !void { | |
| 60 | for ([_]u32{ 0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000 }) |v| try s.int32(v); | |
| 61 | } | |
| 62 | }; | |
| 63 | pub const Nals = struct { | |
| 64 | data: []const u8, | |
| 65 | at: usize = 0, | |
| 66 | pub fn next(s: *Nals) !?[]const u8 { | |
| 67 | if (s.at == s.data.len) return null; | |
| 68 | if (s.data.len - s.at < 4) return error.InvalidNal; | |
| 69 | const n = std.mem.readInt(u32, s.data[s.at..][0..4], .big); | |
| 70 | s.at += 4; | |
| 71 | if (n == 0 or n > s.data.len - s.at) return error.InvalidNal; | |
| 72 | const nal = s.data[s.at..][0..n]; | |
| 73 | s.at += n; | |
| 74 | return nal; | |
| 75 | } | |
| 76 | }; | |
| 77 | fn config(b: *Buffer, codec: Codec, headers: []const u8) !void { | |
| 78 | var nals: Nals = .{ .data = headers }; | |
| 79 | var sps: []const u8 = &.{}; | |
| 80 | var pps: []const u8 = &.{}; | |
| 81 | var vps: []const u8 = &.{}; | |
| 82 | while (try nals.next()) |nal| { | |
| 83 | if (nal.len > 2048) return error.HeaderTooLarge; | |
| 84 | const kind = if (codec == .h264) nal[0] & 31 else (nal[0] >> 1) & 63; | |
| 85 | if (kind == (if (codec == .h264) @as(u8, 7) else 33)) sps = nal; | |
| 86 | if (kind == (if (codec == .h264) @as(u8, 8) else 34)) pps = nal; | |
| 87 | if (codec == .hevc and kind == 32) vps = nal; | |
| 88 | } | |
| 89 | if (sps.len < 4 or pps.len < 2) return error.MissingParameterSets; | |
| 90 | const at = try b.box(if (codec == .h264) "avcC" else "hvcC"); | |
| 91 | if (codec == .h264) { | |
| 92 | // The adapter fixes AVC to 8-bit 4:2:0 High profile. | |
| 93 | if (sps[1] != 100) return error.UnsupportedProfile; | |
| 94 | try b.bytes(&.{ 1, sps[1], sps[2], sps[3], 255, 225 }); | |
| 95 | try b.int16(@intCast(sps.len)); | |
| 96 | try b.bytes(sps); | |
| 97 | try b.bytes(&.{1}); | |
| 98 | try b.int16(@intCast(pps.len)); | |
| 99 | try b.bytes(pps); | |
| 100 | try b.bytes(&.{ 253, 248, 248, 0 }); // high profile: 4:2:0, 8-bit, no SPS extensions | |
| 101 | } else { | |
| 102 | if (vps.len < 2 or sps.len < 16) return error.MissingParameterSets; | |
| 103 | // Remove emulation prevention before reading SPS profile_tier_level. | |
| 104 | var rbsp: [2048]u8 = undefined; | |
| 105 | var len: usize = 0; | |
| 106 | var zeros: usize = 0; | |
| 107 | for (sps[2..]) |byte| { | |
| 108 | if (zeros >= 2 and byte == 3) { | |
| 109 | zeros = 0; | |
| 110 | continue; | |
| 111 | } | |
| 112 | rbsp[len] = byte; | |
| 113 | len += 1; | |
| 114 | zeros = if (byte == 0) zeros + 1 else 0; | |
| 115 | } | |
| 116 | if (len < 13 or (rbsp[0] & 14) != 0 or (rbsp[1] & 31) != 1) return error.UnsupportedProfile; | |
| 117 | try b.bytes(&.{1}); | |
| 118 | try b.bytes(rbsp[1..13]); | |
| 119 | try b.bytes(&.{ 240, 0, 252, 253, 248, 248, 0, 0, 15, 3 }); | |
| 120 | for ([_]u8{ 32, 33, 34 }, [_][]const u8{ vps, sps, pps }) |kind, nal| { | |
| 121 | try b.bytes(&.{128 | kind}); | |
| 122 | try b.int16(1); | |
| 123 | try b.int16(@intCast(nal.len)); | |
| 124 | try b.bytes(nal); | |
| 125 | } | |
| 126 | } | |
| 127 | b.end(at); | |
| 128 | } | |
| 129 | fn audioTrack(b: *Buffer, cfg: AudioConfig) !AudioOffsets { | |
| 130 | const rates = [_]u32{ 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000 }; | |
| 131 | var index: ?u16 = null; | |
| 132 | for (rates, 0..) |rate, i| if (rate == cfg.sample_rate) { | |
| 133 | index = @intCast(i); | |
| 134 | }; | |
| 135 | if (index == null or cfg.sample_rate > 48000 or cfg.channels < 1 or cfg.channels > 2 or cfg.bitrate < 16000 or cfg.bitrate > 320000 or cfg.delay > 16384) return error.InvalidConfiguration; | |
| 136 | const expected = (@as(u16, 2) << 11) | (index.? << 7) | (@as(u16, @intCast(cfg.channels)) << 3); | |
| 137 | if (std.mem.readInt(u16, &cfg.config, .big) != expected) return error.InvalidConfiguration; | |
| 138 | var offsets: AudioOffsets = .{}; | |
| 139 | const trak = try b.box("trak"); | |
| 140 | const tkhd = try b.full("tkhd", 0x1000007); | |
| 141 | try b.zero(16); | |
| 142 | try b.int32(2); | |
| 143 | try b.zero(4); | |
| 144 | offsets.track = b.len; | |
| 145 | try b.int64(0); | |
| 146 | try b.zero(8); | |
| 147 | try b.zero(4); | |
| 148 | try b.int16(0x100); | |
| 149 | try b.int16(0); | |
| 150 | try b.matrix(); | |
| 151 | try b.zero(8); | |
| 152 | b.end(tkhd); | |
| 153 | const edts = try b.box("edts"); | |
| 154 | const elst = try b.full("elst", 0x1000000); | |
| 155 | try b.int32(2); | |
| 156 | offsets.empty = b.len; | |
| 157 | try b.int64(0); | |
| 158 | try b.int64(std.math.maxInt(u64)); | |
| 159 | try b.int16(1); | |
| 160 | try b.int16(0); | |
| 161 | offsets.edit = b.len; | |
| 162 | try b.int64(0); | |
| 163 | try b.int64(cfg.delay); | |
| 164 | try b.int16(1); | |
| 165 | try b.int16(0); | |
| 166 | b.end(elst); | |
| 167 | b.end(edts); | |
| 168 | const mdia = try b.box("mdia"); | |
| 169 | const mdhd = try b.full("mdhd", 0x1000000); | |
| 170 | try b.zero(16); | |
| 171 | try b.int32(cfg.sample_rate); | |
| 172 | offsets.media = b.len; | |
| 173 | try b.int64(0); | |
| 174 | try b.int16(0x55c4); | |
| 175 | try b.int16(0); | |
| 176 | b.end(mdhd); | |
| 177 | const hdlr = try b.full("hdlr", 0); | |
| 178 | try b.int32(0); | |
| 179 | try b.bytes("soun"); | |
| 180 | try b.zero(12); | |
| 181 | try b.bytes("Home AAC\x00"); | |
| 182 | b.end(hdlr); | |
| 183 | const minf = try b.box("minf"); | |
| 184 | const smhd = try b.full("smhd", 0); | |
| 185 | try b.zero(4); | |
| 186 | b.end(smhd); | |
| 187 | const dinf = try b.box("dinf"); | |
| 188 | const dref = try b.full("dref", 0); | |
| 189 | try b.int32(1); | |
| 190 | const url = try b.full("url ", 1); | |
| 191 | b.end(url); | |
| 192 | b.end(dref); | |
| 193 | b.end(dinf); | |
| 194 | const stbl = try b.box("stbl"); | |
| 195 | const stsd = try b.full("stsd", 0); | |
| 196 | try b.int32(1); | |
| 197 | const entry = try b.box("mp4a"); | |
| 198 | try b.zero(6); | |
| 199 | try b.int16(1); | |
| 200 | try b.zero(8); | |
| 201 | try b.int16(@intCast(cfg.channels)); | |
| 202 | try b.int16(16); | |
| 203 | try b.zero(4); | |
| 204 | try b.int32(cfg.sample_rate << 16); | |
| 205 | const esds = try b.full("esds", 0); | |
| 206 | try b.bytes(&.{ 3, 25 }); | |
| 207 | try b.int16(2); | |
| 208 | try b.bytes(&.{ 0, 4, 17, 0x40, 0x15, 0, 0x20, 0 }); | |
| 209 | try b.int32(cfg.bitrate); | |
| 210 | try b.int32(cfg.bitrate); | |
| 211 | try b.bytes(&.{ 5, 2 }); | |
| 212 | try b.bytes(&cfg.config); | |
| 213 | try b.bytes(&.{ 6, 1, 2 }); | |
| 214 | b.end(esds); | |
| 215 | b.end(entry); | |
| 216 | b.end(stsd); | |
| 217 | for ([_]*const [4]u8{ "stts", "stsc", "stco" }) |name| { | |
| 218 | const at = try b.full(name, 0); | |
| 219 | try b.int32(0); | |
| 220 | b.end(at); | |
| 221 | } | |
| 222 | const stsz = try b.full("stsz", 0); | |
| 223 | try b.zero(8); | |
| 224 | b.end(stsz); | |
| 225 | // Explicit decoder pre-roll makes the edit list authoritative. Without | |
| 226 | // roll sample groups Apple assumes an additional legacy 2112-sample delay. | |
| 227 | const sgpd = try b.full("sgpd", 0x1000000); | |
| 228 | try b.bytes("roll"); | |
| 229 | try b.int32(2); | |
| 230 | try b.int32(1); | |
| 231 | try b.int16(65535); | |
| 232 | b.end(sgpd); | |
| 233 | const sbgp = try b.full("sbgp", 0); | |
| 234 | try b.bytes("roll"); | |
| 235 | try b.int32(0); | |
| 236 | b.end(sbgp); | |
| 237 | b.end(stbl); | |
| 238 | b.end(minf); | |
| 239 | b.end(mdia); | |
| 240 | b.end(trak); | |
| 241 | return offsets; | |
| 242 | } | |
| 243 | pub const Writer = struct { | |
| 244 | sink: Sink, | |
| 245 | sequence: u32 = 1, | |
| 246 | last_dts: u64 = 0, | |
| 247 | frames: u64 = 0, | |
| 248 | failed: bool = false, | |
| 249 | finished: bool = false, | |
| 250 | audio_config: ?AudioConfig = null, | |
| 251 | audio_packets: u64 = 0, | |
| 252 | audio_pending: [8192]u8 = undefined, | |
| 253 | audio_pending_len: usize = 0, | |
| 254 | video_end_us: u64 = 0, | |
| 255 | movie_duration: usize = 0, | |
| 256 | video_duration: usize = 0, | |
| 257 | video_media_duration: usize = 0, | |
| 258 | audio_offsets: AudioOffsets = .{}, | |
| 259 | pub fn initAv(sink: Sink, width: u32, height: u32, codec: Codec, headers: []const u8, audio_cfg: AudioConfig) !Writer { | |
| 260 | if (sink.patch == null) return error.InvalidConfiguration; | |
| 261 | return initInternal(sink, width, height, codec, headers, audio_cfg); | |
| 262 | } | |
| 263 | pub fn init(sink: Sink, width: u32, height: u32, codec: Codec, headers: []const u8) !Writer { | |
| 264 | return initInternal(sink, width, height, codec, headers, null); | |
| 265 | } | |
| 266 | fn initInternal(sink: Sink, width: u32, height: u32, codec: Codec, headers: []const u8, audio_cfg: ?AudioConfig) !Writer { | |
| 267 | if (width == 0 or height == 0 or width > 8192 or height > 8192) return error.InvalidConfiguration; | |
| 268 | var b: Buffer = .{}; | |
| 269 | const ftyp = try b.box("ftyp"); | |
| 270 | try b.bytes("iso6"); | |
| 271 | try b.int32(512); | |
| 272 | try b.bytes("iso6isommp41"); | |
| 273 | b.end(ftyp); | |
| 274 | const moov = try b.box("moov"); | |
| 275 | const mvhd = try b.full("mvhd", 0x1000000); | |
| 276 | try b.zero(16); | |
| 277 | try b.int32(1000000); | |
| 278 | const movie_duration = b.len; | |
| 279 | try b.int64(0); | |
| 280 | try b.int32(0x10000); | |
| 281 | try b.int16(0x100); | |
| 282 | try b.zero(10); | |
| 283 | try b.matrix(); | |
| 284 | try b.zero(24); | |
| 285 | try b.int32(if (audio_cfg != null) 3 else 2); | |
| 286 | b.end(mvhd); | |
| 287 | const trak = try b.box("trak"); | |
| 288 | const tkhd = try b.full("tkhd", 0x1000007); | |
| 289 | try b.zero(16); | |
| 290 | try b.int32(1); | |
| 291 | try b.zero(4); | |
| 292 | const video_duration = b.len; | |
| 293 | try b.int64(0); | |
| 294 | try b.zero(8); | |
| 295 | try b.zero(8); | |
| 296 | try b.matrix(); | |
| 297 | try b.int32(width << 16); | |
| 298 | try b.int32(height << 16); | |
| 299 | b.end(tkhd); | |
| 300 | const mdia = try b.box("mdia"); | |
| 301 | const mdhd = try b.full("mdhd", 0x1000000); | |
| 302 | try b.zero(16); | |
| 303 | try b.int32(1000000); | |
| 304 | const video_media_duration = b.len; | |
| 305 | try b.int64(0); | |
| 306 | try b.int16(0x55c4); | |
| 307 | try b.int16(0); | |
| 308 | b.end(mdhd); | |
| 309 | const hdlr = try b.full("hdlr", 0); | |
| 310 | try b.int32(0); | |
| 311 | try b.bytes("vide"); | |
| 312 | try b.zero(12); | |
| 313 | try b.bytes("Home Video\x00"); | |
| 314 | b.end(hdlr); | |
| 315 | const minf = try b.box("minf"); | |
| 316 | const vmhd = try b.full("vmhd", 1); | |
| 317 | try b.zero(8); | |
| 318 | b.end(vmhd); | |
| 319 | const dinf = try b.box("dinf"); | |
| 320 | const dref = try b.full("dref", 0); | |
| 321 | try b.int32(1); | |
| 322 | const url = try b.full("url ", 1); | |
| 323 | b.end(url); | |
| 324 | b.end(dref); | |
| 325 | b.end(dinf); | |
| 326 | const stbl = try b.box("stbl"); | |
| 327 | const stsd = try b.full("stsd", 0); | |
| 328 | try b.int32(1); | |
| 329 | const entry = try b.box(if (codec == .h264) "avc1" else "hvc1"); | |
| 330 | try b.zero(6); | |
| 331 | try b.int16(1); | |
| 332 | try b.zero(16); | |
| 333 | try b.int16(@intCast(width)); | |
| 334 | try b.int16(@intCast(height)); | |
| 335 | try b.int32(0x480000); | |
| 336 | try b.int32(0x480000); | |
| 337 | try b.int32(0); | |
| 338 | try b.int16(1); | |
| 339 | try b.zero(32); | |
| 340 | try b.int16(24); | |
| 341 | try b.int16(65535); | |
| 342 | try config(&b, codec, headers); | |
| 343 | const colr = try b.box("colr"); | |
| 344 | try b.bytes("nclx"); | |
| 345 | try b.int16(1); | |
| 346 | try b.int16(13); | |
| 347 | try b.int16(6); | |
| 348 | try b.bytes(&.{0}); | |
| 349 | b.end(colr); | |
| 350 | b.end(entry); | |
| 351 | b.end(stsd); | |
| 352 | for ([_]*const [4]u8{ "stts", "stsc", "stco" }) |name| { | |
| 353 | const at = try b.full(name, 0); | |
| 354 | try b.int32(0); | |
| 355 | b.end(at); | |
| 356 | } | |
| 357 | const stsz = try b.full("stsz", 0); | |
| 358 | try b.zero(8); | |
| 359 | b.end(stsz); | |
| 360 | b.end(stbl); | |
| 361 | b.end(minf); | |
| 362 | b.end(mdia); | |
| 363 | b.end(trak); | |
| 364 | const offsets = if (audio_cfg) |cfg| try audioTrack(&b, cfg) else AudioOffsets{}; | |
| 365 | const mvex = try b.box("mvex"); | |
| 366 | const trex = try b.full("trex", 0); | |
| 367 | try b.int32(1); | |
| 368 | try b.int32(1); | |
| 369 | try b.zero(12); | |
| 370 | b.end(trex); | |
| 371 | if (audio_cfg != null) { | |
| 372 | const audio_trex = try b.full("trex", 0); | |
| 373 | try b.int32(2); | |
| 374 | try b.int32(1); | |
| 375 | try b.int32(1024); | |
| 376 | try b.int32(0); | |
| 377 | try b.int32(0x02000000); | |
| 378 | b.end(audio_trex); | |
| 379 | } | |
| 380 | b.end(mvex); | |
| 381 | b.end(moov); | |
| 382 | try sink.write(sink.context, b.slice()); | |
| 383 | return .{ .sink = sink, .audio_config = audio_cfg, .audio_offsets = offsets, .movie_duration = movie_duration, .video_duration = video_duration, .video_media_duration = video_media_duration }; | |
| 384 | } | |
| 385 | /// One complete sample. Caller supplies its actual duration, using the next | |
| 386 | /// capture time; the last sample uses nominal frame duration. PTS == DTS. | |
| 387 | pub fn frame(s: *Writer, sample: []const u8, dts_us: u64, duration_us: u32, key: bool) !void { | |
| 388 | if (s.failed or s.finished) return error.WriterFailed; | |
| 389 | 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; | |
| 390 | var nals: Nals = .{ .data = sample }; | |
| 391 | while (try nals.next()) |_| {} | |
| 392 | const end = try std.math.add(u64, dts_us, duration_us); | |
| 393 | try s.fragment(1, sample, dts_us, duration_us, key); | |
| 394 | s.frames += 1; | |
| 395 | s.last_dts = dts_us; | |
| 396 | s.video_end_us = end; | |
| 397 | } | |
| 398 | pub fn audio(s: *Writer, sample: []const u8) !void { | |
| 399 | if (s.audio_config == null or s.failed or s.finished) return error.WriterFailed; | |
| 400 | if (sample.len == 0 or sample.len > 8192 or s.sequence == std.math.maxInt(u32)) return error.InvalidSample; | |
| 401 | if (s.audio_pending_len != 0) try s.flushAudio(1024); | |
| 402 | @memcpy(s.audio_pending[0..sample.len], sample); | |
| 403 | s.audio_pending_len = sample.len; | |
| 404 | } | |
| 405 | fn flushAudio(s: *Writer, duration: u32) !void { | |
| 406 | if (s.audio_pending_len == 0) return; | |
| 407 | const time = try std.math.mul(u64, s.audio_packets, 1024); | |
| 408 | try s.fragment(2, s.audio_pending[0..s.audio_pending_len], time, duration, true); | |
| 409 | s.audio_pending_len = 0; | |
| 410 | s.audio_packets += 1; | |
| 411 | } | |
| 412 | /// Patch fixed metadata only after draining AAC. Decoder delay and tail | |
| 413 | /// padding stay in media samples; edit lists expose exactly the input PCM. | |
| 414 | pub fn finish(s: *Writer, audio_start_us: u64, presented_frames: u64) !void { | |
| 415 | if (s.failed or s.finished) return error.WriterFailed; | |
| 416 | const cfg = s.audio_config orelse return error.InvalidConfiguration; | |
| 417 | const encoded = try std.math.mul(u64, s.audio_packets + @intFromBool(s.audio_pending_len != 0), 1024); | |
| 418 | const valid_media = try std.math.add(u64, presented_frames, cfg.delay); | |
| 419 | if (encoded < valid_media or encoded - valid_media >= 1024) return error.InvalidSample; | |
| 420 | const padding = encoded - valid_media; | |
| 421 | const scaled = @as(u128, presented_frames) * 1000000 / cfg.sample_rate; | |
| 422 | if (scaled > std.math.maxInt(u64)) return error.InvalidSample; | |
| 423 | const duration: u64 = @intCast(scaled); | |
| 424 | const audio_end = try std.math.add(u64, audio_start_us, duration); | |
| 425 | errdefer s.failed = true; | |
| 426 | // A short final duration also signals tail trim to fragmented readers | |
| 427 | // that do not apply the finite edit duration to the last fragment. | |
| 428 | try s.flushAudio(@intCast(1024 - padding)); | |
| 429 | try s.patch64(s.movie_duration, @max(s.video_end_us, audio_end)); | |
| 430 | try s.patch64(s.video_duration, s.video_end_us); | |
| 431 | try s.patch64(s.video_media_duration, s.video_end_us); | |
| 432 | try s.patch64(s.audio_offsets.track, audio_end); | |
| 433 | try s.patch64(s.audio_offsets.media, valid_media); | |
| 434 | try s.patch64(s.audio_offsets.empty, audio_start_us); | |
| 435 | try s.patch64(s.audio_offsets.edit, duration); | |
| 436 | s.finished = true; | |
| 437 | } | |
| 438 | fn patch64(s: *Writer, offset: usize, value: u64) !void { | |
| 439 | var bytes: [8]u8 = undefined; | |
| 440 | std.mem.writeInt(u64, &bytes, value, .big); | |
| 441 | try s.sink.patch.?(s.sink.context, offset, &bytes); | |
| 442 | } | |
| 443 | fn fragment(s: *Writer, track: u32, sample: []const u8, time: u64, duration: u32, key: bool) !void { | |
| 444 | if (s.failed or s.finished) return error.WriterFailed; | |
| 445 | if (s.sequence == std.math.maxInt(u32)) return error.InvalidSample; | |
| 446 | var b: Buffer = .{}; | |
| 447 | const moof = try b.box("moof"); | |
| 448 | const mfhd = try b.full("mfhd", 0); | |
| 449 | try b.int32(s.sequence); | |
| 450 | b.end(mfhd); | |
| 451 | const traf = try b.box("traf"); | |
| 452 | const tfhd = try b.full("tfhd", 0x20000); | |
| 453 | try b.int32(track); | |
| 454 | b.end(tfhd); | |
| 455 | const tfdt = try b.full("tfdt", 0x1000000); | |
| 456 | try b.int64(time); | |
| 457 | b.end(tfdt); | |
| 458 | const trun = try b.full("trun", 0x701); | |
| 459 | try b.int32(1); | |
| 460 | const offset = b.len; | |
| 461 | try b.int32(0); | |
| 462 | try b.int32(duration); | |
| 463 | try b.int32(@intCast(sample.len)); | |
| 464 | try b.int32(if (key) 0x02000000 else 0x01010000); | |
| 465 | b.end(trun); | |
| 466 | if (track == 2) { | |
| 467 | const sbgp = try b.full("sbgp", 0); | |
| 468 | try b.bytes("roll"); | |
| 469 | try b.int32(1); | |
| 470 | try b.int32(1); | |
| 471 | try b.int32(1); | |
| 472 | b.end(sbgp); | |
| 473 | } | |
| 474 | b.end(traf); | |
| 475 | b.end(moof); | |
| 476 | std.mem.writeInt(u32, b.data[offset..][0..4], @intCast(b.len + 8), .big); | |
| 477 | try b.int32(@intCast(sample.len + 8)); | |
| 478 | try b.bytes("mdat"); | |
| 479 | errdefer s.failed = true; | |
| 480 | try s.sink.write(s.sink.context, b.slice()); | |
| 481 | try s.sink.write(s.sink.context, sample); | |
| 482 | s.sequence += 1; | |
| 483 | } | |
| 484 | }; | |
| 485 | test "NAL lengths reject truncation and empty units" { | |
| 486 | var n: Nals = .{ .data = &.{ 0, 0, 0, 2, 0x65 } }; | |
| 487 | try std.testing.expectError(error.InvalidNal, n.next()); | |
| 488 | n = .{ .data = &.{ 0, 0, 0, 0 } }; | |
| 489 | try std.testing.expectError(error.InvalidNal, n.next()); | |
| 490 | } | |
| 491 | test "fragment sizes, offsets and 64-bit time are exact; sink failure poisons writer" { | |
| 492 | const Out = struct { | |
| 493 | data: std.ArrayList(u8) = .empty, | |
| 494 | fail: bool = false, | |
| 495 | fn write(ctx: *anyopaque, bytes: []const u8) anyerror!void { | |
| 496 | const s: *@This() = @ptrCast(@alignCast(ctx)); | |
| 497 | if (s.fail) return error.DiskFull; | |
| 498 | try s.data.appendSlice(std.testing.allocator, bytes); | |
| 499 | } | |
| 500 | }; | |
| 501 | var out: Out = .{}; | |
| 502 | defer out.data.deinit(std.testing.allocator); | |
| 503 | var writer: Writer = .{ .sink = .{ .context = &out, .write = Out.write } }; | |
| 504 | try writer.frame(&.{ 0, 0, 0, 2, 0x65, 0x80 }, 0x100000000, 41667, true); | |
| 505 | const bytes = out.data.items; | |
| 506 | const size = std.mem.readInt(u32, bytes[0..4], .big); | |
| 507 | try std.testing.expectEqualStrings("moof", bytes[4..8]); | |
| 508 | try std.testing.expectEqual(@as(u32, 114), std.mem.readInt(u32, bytes[size..][0..4], .big) + size); | |
| 509 | try std.testing.expectEqual(@as(u64, 0x100000000), std.mem.readInt(u64, bytes[60..68], .big)); | |
| 510 | try std.testing.expectEqual(size + 8, std.mem.readInt(u32, bytes[84..88], .big)); | |
| 511 | try std.testing.expectError(error.InvalidSample, writer.frame(&.{ 0, 0, 0, 1, 1 }, 0x100000000, 1, false)); | |
| 512 | out.fail = true; | |
| 513 | try std.testing.expectError(error.DiskFull, writer.frame(&.{ 0, 0, 0, 1, 1 }, 0x100000001, 1, false)); | |
| 514 | try std.testing.expectError(error.WriterFailed, writer.frame(&.{ 0, 0, 0, 1, 1 }, 0x100000002, 1, false)); | |
| 515 | } | |
| 516 | ||
| 517 | /// CodecPrivate for Matroska, without the enclosing avcC/hvcC box header. | |
| 518 | pub fn codecPrivate(codec: Codec, headers: []const u8, destination: []u8) ![]const u8 { | |
| 519 | var b: Buffer = .{}; | |
| 520 | try config(&b, codec, headers); | |
| 521 | const data = b.slice()[8..]; | |
| 522 | if (destination.len < data.len) return error.HeaderTooLarge; | |
| 523 | @memcpy(destination[0..data.len], data); | |
| 524 | return destination[0..data.len]; | |
| 525 | } | |
| 526 | test "Matroska CodecPrivate reuses checked config and respects destination bounds" { | |
| 527 | const headers = [_]u8{ 0, 0, 0, 4, 0x67, 100, 0, 40, 0, 0, 0, 2, 0x68, 0x80 }; | |
| 528 | var destination: [128]u8 = undefined; | |
| 529 | const data = try codecPrivate(.h264, &headers, &destination); | |
| 530 | try std.testing.expectEqualSlices(u8, &.{ 1, 100, 0, 40, 255, 225 }, data[0..6]); | |
| 531 | try std.testing.expectError(error.HeaderTooLarge, codecPrivate(.h264, &headers, destination[0..1])); | |
| 532 | try std.testing.expectError(error.MissingParameterSets, codecPrivate(.h264, &.{}, &destination)); | |
| 533 | } | |
| 534 | test "AAC track sample clock, edit delay/tail, fixed patches and sink failure" { | |
| 535 | const Out = struct { | |
| 536 | bytes: std.ArrayList(u8) = .empty, | |
| 537 | fail: bool = false, | |
| 538 | fn write(ctx: *anyopaque, data: []const u8) anyerror!void { | |
| 539 | const s: *@This() = @ptrCast(@alignCast(ctx)); | |
| 540 | try s.bytes.appendSlice(std.testing.allocator, data); | |
| 541 | } | |
| 542 | fn patch(ctx: *anyopaque, offset: u64, data: []const u8) anyerror!void { | |
| 543 | const s: *@This() = @ptrCast(@alignCast(ctx)); | |
| 544 | if (s.fail) return error.DiskFull; | |
| 545 | if (offset + data.len > s.bytes.items.len) return error.InvalidOffset; | |
| 546 | @memcpy(s.bytes.items[@intCast(offset)..][0..data.len], data); | |
| 547 | } | |
| 548 | }; | |
| 549 | var out: Out = .{}; | |
| 550 | defer out.bytes.deinit(std.testing.allocator); | |
| 551 | const headers = [_]u8{ 0, 0, 0, 4, 0x67, 100, 0, 40, 0, 0, 0, 2, 0x68, 0x80 }; | |
| 552 | const cfg: AudioConfig = .{ .sample_rate = 48000, .channels = 2, .bitrate = 128000, .delay = 2048, .config = .{ 0x11, 0x90 } }; | |
| 553 | const sink: Sink = .{ .context = &out, .write = Out.write, .patch = Out.patch }; | |
| 554 | var writer = try Writer.initAv(sink, 128, 96, .h264, &headers, cfg); | |
| 555 | try std.testing.expect(std.mem.indexOf(u8, out.bytes.items, "mp4a") != null); | |
| 556 | try writer.frame(&.{ 0, 0, 0, 2, 0x65, 0x80 }, 0, 41667, true); | |
| 557 | try std.testing.expectError(error.InvalidSample, writer.finish(12345, 1000)); | |
| 558 | for (0..3) |_| try writer.audio(&.{ 0x21, 0x10 }); | |
| 559 | try writer.finish(12345, 1000); | |
| 560 | try std.testing.expectEqual(@as(u64, 12345), std.mem.readInt(u64, out.bytes.items[writer.audio_offsets.empty..][0..8], .big)); | |
| 561 | try std.testing.expectEqual(@as(u64, 20833), std.mem.readInt(u64, out.bytes.items[writer.audio_offsets.edit..][0..8], .big)); | |
| 562 | try std.testing.expectEqual(@as(u64, 2048), std.mem.readInt(u64, out.bytes.items[writer.audio_offsets.edit + 8 ..][0..8], .big)); | |
| 563 | try std.testing.expectEqual(@as(u64, 3048), std.mem.readInt(u64, out.bytes.items[writer.audio_offsets.media..][0..8], .big)); | |
| 564 | try std.testing.expectError(error.WriterFailed, writer.audio(&.{1})); | |
| 565 | var failure = try Writer.initAv(sink, 128, 96, .h264, &headers, cfg); | |
| 566 | try failure.audio(&.{ 0x21, 0x10 }); | |
| 567 | try failure.audio(&.{ 0x21, 0x10 }); | |
| 568 | out.fail = true; | |
| 569 | try std.testing.expectError(error.DiskFull, failure.finish(0, 0)); | |
| 570 | try std.testing.expectError(error.WriterFailed, failure.audio(&.{1})); | |
| 571 | } | |
| @@ -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 | |
| @@ -0,0 +1,38 @@ | ||
| 1 | //! Standalone adapter test; link native_encoder.c with system x264/x265. | |
| 2 | const std = ; | |
| 3 | const native = ; | |
| 4 | fn observe(packet: native.Packet, codec: u32, seen: *[8]bool) !void { | |
| 5 | const index: usize = @intCast(@divTrunc(packet.pts, 41667)); | |
| 6 | try std.testing.expect(index < 8 and !seen[index]); | |
| 7 | seen[index] = true; | |
| 8 | const key = index == 0 or index == 3 or index == 6; | |
| 9 | try std.testing.expectEqual(key, packet.key); | |
| 10 | var at: usize = 0; | |
| 11 | var idr = false; | |
| 12 | while (at < packet.data.len) { | |
| 13 | try std.testing.expect(packet.data.len - at >= 4); | |
| 14 | const n = std.mem.readInt(u32, packet.data[at..][0..4], .big); | |
| 15 | at += 4; | |
| 16 | try std.testing.expect(n > 0 and n <= packet.data.len - at); | |
| 17 | const kind = if (codec == 1) packet.data[at] & 31 else (packet.data[at] >> 1) & 63; | |
| 18 | idr = idr or (if (codec == 1) kind == 5 else kind == 19 or kind == 20); | |
| 19 | at += n; | |
| 20 | } | |
| 21 | try std.testing.expectEqual(key, idr); | |
| 22 | } | |
| 23 | test "requested IDRs override long GOP without losing delayed AVC frames" { | |
| 24 | const y: [128 * 96]u8 = @splat(64); | |
| 25 | const uv: [64 * 48]u8 = @splat(128); | |
| 26 | for ([_]u32{ 1, 2 }) |codec| { | |
| 27 | var encoder = try native.Encoder.init(.{ .codec = codec, .width = 128, .height = 96, .fps_num = 24, .fps_den = 1, .bitrate = 0, .quality = 90, .gop = 100 }); | |
| 28 | defer encoder.deinit(); | |
| 29 | var seen: [8]bool = @splat(false); | |
| 30 | for (0..8) |i| { | |
| 31 | if (i == 3 or i == 6) try encoder.requestKeyframe(); | |
| 32 | if (try encoder.encode(.{ &y, &uv, &uv }, .{ 128, 64, 64 }, 128, 96, @intCast(i * 41667))) |packet| try observe(packet, codec, &seen); | |
| 33 | } | |
| 34 | while (try encoder.flush()) |packet| try observe(packet, codec, &seen); | |
| 35 | for (seen) |value| try std.testing.expect(value); | |
| 36 | try std.testing.expectError(error.EncoderFailed, encoder.requestKeyframe()); | |
| 37 | } | |
| 38 | } | |