<?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 ); if ( substr( $data, 0, 3 ) == 'ID3' ) { echo "SKIPPING already has ID3\n"; } else { echo "ADDING ID3\n"; file_put_contents( $filename, "$tag$data" ); } } $playlist = "caminandes-llamigos.webm.audio.mp3.m3u8"; $lines = file( $playlist, FILE_IGNORE_NEW_LINES + FILE_SKIP_EMPTY_LINES ); $pts = 0.0; $duration = 0.0; foreach ( $lines as $line ) { /* #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 ... */ $matches = null; if ( preg_match( '/^#EXTINF:\s*(\d+(?:\.\d+)?),/', $line, $matches ) ) { $duration = floatval( $matches[1] ); continue; } else if (preg_match( '/^#/', $line ) ) { continue; } $filename = $line; process_mp3( $filename, $pts ); $pts += $duration; $duration = 0; }