<?php /* PRIV frame type should contain: The ID3 PRIV owner identifier MUST be "com.apple.streaming.transportStreamTimestamp". The ID3 payload MUST be a 33-bit MPEG-2 Program Elementary Stream timestamp expressed as a big-endian eight-octet number, with the upper 31 bits set to zero. Clients SHOULD NOT play Packed Audio Segments without this ID3 tag. https://id3.org/id3v2.4.0-frames https://id3.org/id3v2.4.0-structure bit order is MSB first, big-endian header 10 bytes extended header (var, optional) frames (variable) pading (variable, optional) footer (10 bytes, optional) header: "ID3" version: 16 bits $04 00 flags: 32 bits idv2 size: 32 bits (in chunks of 4 bytes, not counting header or footer) flags: bit 7 - unsyncrhonization (??) bit 6 - extended header bit 5 - experimental indicator bit 4 - footer present frame: id - 32 bits (four chars) size - 32 bits (in chunks of 4 bytes, excluding frame header) flags - 16 bits (frame data) priv payload: owner text string followed by \x00 (binary data) The timestamps... I think... have 90 kHz integer resolution so convert from the decimal seconds in the HLS */ function hexdump($str) { $len = strlen( $str ); return unpack("H*", $str)[1]; } const KHZ_90 = 90000; const MHZ_27 = 27000000; function process_mp3( $filename, $pts ) { $owner = "com.apple.streaming.transportStreamTimestamp\x00"; $timestamp = $pts * KHZ_90; $timestamp_high = 0; $timestamp_low = intval( $timestamp ); // assume they won't get too big for 31 bits $frame_data = pack( 'a*NN', $owner, $timestamp_high, $timestamp_low, ); $frame_type = 'PRIV'; $frame_flags = 0; $frame_length = strlen( $frame_data ); // if >127 bytes may need to adjust $frame = pack( 'a4Nna*', $frame_type, $frame_length, $frame_flags, $frame_data ); $tag_type = 'ID3'; $tag_version = 0x0400; $tag_flags = 0; $tag_length = strlen( $frame ); // if >127 bytes may need to adjust $tag = pack( 'a3nCNa*', $tag_type, $tag_version, $tag_flags, $tag_length, $frame ); $hex = hexdump($tag); print "$filename $pts $hex\n"; $data = file_get_contents( $filename ); return "$tag$data"; } $playlist = "caminandes-llamigos.webm.audio.mp3.m3u8"; $playlist_out = "caminandes-llamigos.webm.audio.mp3.combined.m3u8"; $outfile = "caminandes-llamigos.webm.audio.mp3"; $lines = file( $playlist, FILE_IGNORE_NEW_LINES + FILE_SKIP_EMPTY_LINES ); $pts = 0.0; $duration = 0.0; $lines_out = []; $chunks = []; $offset = 0; foreach ( $lines as $line ) { // todo: create a single-file version // and rewrite the manifest /* #EXTM3U #EXT-X-VERSION:3 #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-ALLOW-CACHE:YES #EXT-X-TARGETDURATION:11 #EXTINF:10.005397, caminandes-llamigos.webm.audio.mp3.0000.mp3 #EXTINF:10.004898, caminandes-llamigos.webm.audio.mp3.0001.mp3 ... for output: #EXT-X-BYTERANGE:132872@730 */ $matches = null; if ( preg_match( '/^#EXTINF:\s*(\d+(?:\.\d+)?),/', $line, $matches ) ) { $duration = floatval( $matches[1] ); } if ( preg_match( '/^#EXT-X-VERSION:(.*)/', $line, $matches ) ) { if ( intval( $matches[1] ) < 4 ) { $line = "#EXT-X-VERSION:7"; } } if (preg_match( '/^#/', $line ) ) { $lines_out[] = $line; continue; } $filename = $line; $chunk = process_mp3( $filename, $pts ); $len = strlen( $chunk ); $lines_out[] = "#EXT-X-BYTERANGE:$len@$offset"; $lines_out[] = "$outfile"; $chunks[] = $chunk; $offset += $len; $pts += $duration; $duration = 0; } file_put_contents( $outfile, implode( '', $chunks ) ); file_put_contents( $playlist_out, implode( "\n", $lines_out ) );