Media.PackageHls("in.mp4", "hls") cuts a video into HLS segments and writes the playlists beside them: CMAF (fragmented MP4) segments by default, one directory per track, a media playlist (index.m3u8) for each, and a multivariant playlist (master.m3u8) that points a player at them. Nothing is re-encoded: the segments carry the MP4’s own coded pictures and audio frames. Packaging is new in 0.7.0-preview.1 and is a preview.
A preview that no validator has passed. In the README’s words:
- “No validator has passed the output. Apple
mediastreamvalidatorwas not run (it needs an Apple ID sign-in). The DASH-IF conformance tool v3.1.0 ran once (two findings, both fixed) and then fails in its own base-URI handling on every run.”- “What did check it: packet equality with the input through ffprobe (28 CMAF and 19 transport-stream inputs, 0 unexplained), TSDuck continuity on 38 of 38 transport-stream segment sets, and playback in hls.js 1.7.4 and dash.js 5.2.1 in Chrome 155, Firefox 141 and Playwright WebKit 26 (one failure: HEVC in transport-stream segments through hls.js in Chrome, not investigated) plus native HLS in macOS WebKit (7 of 7).”
dotnet new console -o Mp4ToHls && cd Mp4ToHls dotnet add package RecursiveAI.OneMedia --version 0.7.0-preview.1 curl -L -o sintel.mp4 https://download.blender.org/durian/trailer/sintel_trailer-480p.mp4
The input is the Sintel trailer (© copyright Blender Foundation, durian.blender.org, CC BY 3.0): 52 seconds of H.264 video and AAC audio. Put this in Program.cs and run dotnet run:
using RecursiveAI.OneMedia; using RecursiveAI.OneMedia.Streaming; // CMAF segments of about six seconds, cut on keyframes, and HLS playlists. PackagingReport report = Media.PackageHls("sintel.mp4", "hls"); foreach (PackagedTrack track in report.Tracks) Console.WriteLine($"{track.Name}: {track.Kind} {track.Codecs}, {track.Segments} segments, " + $"{track.Bytes:N0} bytes, {track.DurationSeconds:0.###} s"); foreach (string playlist in report.Manifests) Console.WriteLine($"wrote {playlist}"); foreach (var loss in report.Losses) Console.WriteLine($"not carried: {loss.Kind} {loss.Subject}: {loss.Reason}"); foreach (string note in report.Notes) Console.WriteLine($"note: {note}");
video0: Video avc1.64001E, 6 segments, 3,525,834 bytes, 52.208 s audio0: Audio mp4a.40.2, 6 segments, 842,751 bytes, 51.947 s wrote video0/index.m3u8 wrote audio0/index.m3u8 wrote master.m3u8
The video and the audio each become six segments and a media playlist. To change the segment length, the segment format or the output, call Packager.Package(input, directory, new PackagingOptions { … }) with SegmentDuration, SegmentFormat or SingleFile.
The playlists and segments
find hls -type f -printf '%8s %p\n' | sort -k2346 hls/audio0/index.m3u8 604 hls/audio0/init.mp4 152426 hls/audio0/seg-00001.m4s 118240 hls/audio0/seg-00002.m4s 118932 hls/audio0/seg-00003.m4s 137160 hls/audio0/seg-00004.m4s 172187 hls/audio0/seg-00005.m4s 143806 hls/audio0/seg-00006.m4s 316 hls/master.m3u8 355 hls/video0/index.m3u8 710 hls/video0/init.mp4 623173 hls/video0/seg-00001.m4s 331892 hls/video0/seg-00002.m4s 776011 hls/video0/seg-00003.m4s 1131306 hls/video0/seg-00004.m4s 576401 hls/video0/seg-00005.m4s 87051 hls/video0/seg-00006.m4s
The multivariant playlist, which is the URL a player is given:
cat hls/master.m3u8#EXTM3U #EXT-X-VERSION:7 #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="Audio 1",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="2",URI="audio0/index.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=1212695,AVERAGE-BANDWIDTH=670058,CODECS="avc1.64001E,mp4a.40.2",RESOLUTION=854x480,AUDIO="audio" video0/index.m3u8
One rendition, with the audio as a separate group. OneMedia doesn’t encode, so it can’t make lower bit-rate copies itself; given several encodes of the same video (Packager.Package(new[] { "1080p.mp4", "720p.mp4" }, "hls")) it writes a bit-rate ladder, one EXT-X-STREAM-INF per input. The video’s media playlist:
cat hls/video0/index.m3u8#EXTM3U #EXT-X-VERSION:7 #EXT-X-TARGETDURATION:11 #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-PLAYLIST-TYPE:VOD #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-MAP:URI="init.mp4" #EXTINF:9.333333, seg-00001.m4s #EXTINF:7.166667, seg-00002.m4s #EXTINF:7.291667, seg-00003.m4s #EXTINF:8.375, seg-00004.m4s #EXTINF:10.416667, seg-00005.m4s #EXTINF:9.625, seg-00006.m4s #EXT-X-ENDLIST
A segment ends at the first keyframe at or after six seconds, so the segments here run 7 to 10 seconds, and EXT-X-TARGETDURATION is the longest, rounded up.
Check it with ffprobe
A CMAF segment plays after its initialization segment. The third video segment, with init.mp4 in front:
cat hls/video0/init.mp4 hls/video0/seg-00003.m4s > seg3.mp4 ffprobe -v error -count_packets -show_entries stream=codec_name,width,height,nb_read_packets -of compact seg3.mp4 ffprobe -v error -show_entries packet=pts_time,flags -of compact seg3.mp4 | head -n 3
stream|codec_name=h264|width=854|height=480|nb_read_packets=175 packet|pts_time=16.500000|flags=K__ packet|pts_time=16.625000|flags=___ packet|pts_time=16.583333|flags=___
The segment starts at 16.5 s, where the first two segments end, on a keyframe (K). Through the multivariant playlist, every packet of both tracks has the time, keyframe flag, size and bytes (MD5) it has in the MP4:
for s in v:0 a:0; do for f in sintel.mp4 hls/master.m3u8; do ffprobe -v error -select_streams $s -show_entries packet=pts_time,flags,size,data_hash -show_data_hash MD5 -of csv=p=0 $f > $(basename $f).csv done cmp -s sintel.mp4.csv master.m3u8.csv && echo "$s: $(wc -l < master.m3u8.csv) packets equal to the MP4's" done
v:0: 1253 packets equal to the MP4's a:0: 2435 packets equal to the MP4's
Play it in a browser
Serve the hls folder from any web server and give the player master.m3u8. Safari and iOS play HLS natively; in Chrome and Firefox, hls.js feeds the segments to the <video> element:
<video id="video" controls muted></video> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/hls.min.js"></script> <script> const video = document.getElementById("video"); if (video.canPlayType("application/vnd.apple.mpegurl")) video.src = "hls/master.m3u8"; else { const hls = new Hls(); hls.loadSource("hls/master.m3u8"); hls.attachMedia(video); } </script>
Served from a local web server, hls.js 1.7.4 in headless Chrome 149 played this output with no errors: 854 × 480, duration 52.208 s, readyState 4, the time advancing from 0.52 s to 2.52 s over two seconds, then a seek to 30 s that landed at 30.000 and went on playing (31.96 s two seconds later). Native HLS in Safari wasn’t run here; the project reports it in macOS WebKit (above).
Transport-stream segments, from the command line
The onemedia tool does the same with onemedia package. --ts writes MPEG-TS segments instead of CMAF, with the video and audio in one stream, for players and devices that want .ts:
dotnet tool install --global RecursiveAI.OneMedia.Tool --version 0.7.0-preview.1onemedia package sintel.mp4 hls-ts --hls --tsstream0 Video avc1.64001E,mp4a.40.2 6 segments, 5084460 bytes, 52.208 s wrote stream0/index.m3u8 wrote master.m3u8 not carried: Disposition NotRepresentable A transport stream has no field for track dispositions. not carried: Disposition NotRepresentable A transport stream has no field for track dispositions.
cat hls-ts/stream0/index.m3u8 | head -n 9 ffprobe -v error -count_packets -show_entries stream=codec_name,nb_read_packets:format=format_name -of compact hls-ts/stream0/seg-00003.ts
#EXTM3U #EXT-X-VERSION:7 #EXT-X-TARGETDURATION:11 #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-PLAYLIST-TYPE:VOD #EXT-X-INDEPENDENT-SEGMENTS #EXTINF:9.333333, seg-00001.ts #EXTINF:7.166667, program|stream|codec_name=h264|nb_read_packets=175 stream|codec_name=aac|nb_read_packets=342 stream|codec_name=h264|nb_read_packets=175 stream|codec_name=aac|nb_read_packets=342 format|format_name=mpegts
The same cuts as the CMAF segments, each .ts starting with PAT and PMT. A TS segment holds the video as Annex B and the audio with ADTS headers, so its packet bytes differ from the MP4’s by design. hls.js 1.7.4 in headless Chrome 149 played these too, with no errors: duration 52.29 s, the time advancing, and a seek to 30 s that kept playing. Leave out --hls to write a DASH manifest as well (Create a DASH MPD from MP4), and add --live to package a stream as it arrives.
Limits
- A preview that no validator has passed. It was checked by packet equality, TSDuck and playback in hls.js and dash.js, and in the README’s words “HEVC in transport-stream segments through hls.js in Chrome” failed and was “not investigated”.
- No encryption. From the README: “encryption is not written; live mode takes one input and holds the current segment in memory”.
- No re-encoding. Segments can only start on the input’s keyframes, so a long keyframe interval gives long segments. A bit-rate ladder needs inputs already encoded at each rate, and “Renditions switch cleanly only when their keyframes coincide” (Packaging).
- TS segments. HLS only; Opus, VP9 and AV1 have no transport-stream mapping the muxer writes and are refused.
- Reading HLS back.
Media.Open("hls/master.m3u8")reads the presentation as one file (Read an HLS stream).