hls-test/timestamp-id3.php

157 lines
3.7 KiB
PHP
Raw Normal View History

2021-10-22 18:27:02 +00:00
<?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 );
2021-11-03 17:43:29 +00:00
return "$tag$data";
2021-10-22 18:27:02 +00:00
}
$playlist = "caminandes-llamigos.webm.audio.mp3.m3u8";
2021-11-03 17:43:29 +00:00
$playlist_out = "caminandes-llamigos.webm.audio.mp3.combined.m3u8";
$outfile = "caminandes-llamigos.webm.audio.mp3";
2021-10-22 18:27:02 +00:00
$lines = file( $playlist, FILE_IGNORE_NEW_LINES + FILE_SKIP_EMPTY_LINES );
$pts = 0.0;
$duration = 0.0;
2021-11-03 17:43:29 +00:00
$lines_out = [];
$chunks = [];
$offset = 0;
2021-10-22 18:27:02 +00:00
foreach ( $lines as $line ) {
2021-11-03 17:43:29 +00:00
// todo: create a single-file version
// and rewrite the manifest
2021-10-22 18:27:02 +00:00
/*
#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
...
2021-11-03 17:43:29 +00:00
for output:
#EXT-X-BYTERANGE:132872@730
2021-10-22 18:27:02 +00:00
*/
$matches = null;
if ( preg_match( '/^#EXTINF:\s*(\d+(?:\.\d+)?),/', $line, $matches ) ) {
$duration = floatval( $matches[1] );
2021-11-03 17:43:29 +00:00
}
if (preg_match( '/^#/', $line ) ) {
$lines_out[] = $line;
2021-10-22 18:27:02 +00:00
continue;
}
$filename = $line;
2021-11-03 17:43:29 +00:00
$chunk = process_mp3( $filename, $pts );
$len = strlen( $chunk );
$lines_out[] = "#EXT-X-BYTERANGE:$len@$offset";
$lines_out[] = "$outfile";
$chunks[] = $chunk;
$offset += $len;
2021-10-22 18:27:02 +00:00
$pts += $duration;
$duration = 0;
}
2021-11-03 17:43:29 +00:00
file_put_contents( $outfile, implode( '', $chunks ) );
file_put_contents( $playlist_out, implode( "\n", $lines_out ) );