Skip to content

Instantly share code, notes, and snippets.

@dinkypumpkin
Last active Mar 3, 2017
Embed
What would you like to do?
#!/usr/bin/env perl
# BBC HLS live stream test harness
# use with Safari or Native HLS Playback extension for Firefox/Chrome
# bbclive.pl daemon
# or
# morbo bbclive.pl
# open http://127.0.0.1:3000/
use Mojolicious::Lite;
use Mojo::JSON qw(decode_json);
use Parse::M3U::Extended qw(m3u_parser);
my $channels = {
'tv' => {
'national' => {
'BBC Four' => 'bbc_four_hd',
'BBC News' => 'bbc_news24',
'BBC One' => 'bbc_one_hd',
'BBC Parliament' => 'bbc_parliament',
'BBC Two' => 'bbc_two_hd',
'CBBC' => 'cbbc_hd',
'CBeebies' => 'cbeebies_hd',
'BBC Radio 1 Live Video' => 'bbc_radio_one_video',
'BBC Radio 1Xtra Live Video' => 'bbc_1xtra_video',
'BBC Radio 5 Live Video' => 'bbc_radio_five_live_video',
},
'regional' => {
'BBC Alba' => 'bbc_alba',
'BBC One Northern Ireland' => 'bbc_one_northern_ireland_hd',
'BBC One Scotland' => 'bbc_one_scotland_hd',
'BBC One Wales' => 'bbc_one_wales_hd',
'BBC Two England' => 'bbc_two_england',
'BBC Two Northern Ireland' => 'bbc_two_northern_ireland_digital',
'BBC Two Scotland' => 'bbc_two_scotland',
'BBC Two Wales' => 'bbc_two_wales_digital',
'S4C' => 's4cpbs',
},
'local' => {
'BBC One Cambridge' => 'bbc_one_cambridge',
'BBC One Channel Islands' => 'bbc_one_channel_islands',
'BBC One East Midlands' => 'bbc_one_east_midlands',
'BBC One East Yorkshire' => 'bbc_one_east_yorkshire',
'BBC One East' => 'bbc_one_east',
'BBC One London' => 'bbc_one_london',
'BBC One North East' => 'bbc_one_north_east',
'BBC One North West' => 'bbc_one_north_west',
'BBC One Oxford' => 'bbc_one_oxford',
'BBC One South East' => 'bbc_one_south_east',
'BBC One South West' => 'bbc_one_south_west',
'BBC One South' => 'bbc_one_south',
'BBC One West Midlands' => 'bbc_one_west_midlands',
'BBC One West' => 'bbc_one_west',
'BBC One Yorks' => 'bbc_one_yorks',
},
'red button' => {
'BBC Red Button 01' => 'sport_stream_01',
'BBC Red Button 02' => 'sport_stream_02',
'BBC Red Button 03' => 'sport_stream_03',
'BBC Red Button 04' => 'sport_stream_04',
'BBC Red Button 05' => 'sport_stream_05',
'BBC Red Button 06' => 'sport_stream_06',
'BBC Red Button 07' => 'sport_stream_07',
'BBC Red Button 08' => 'sport_stream_08',
'BBC Red Button 09' => 'sport_stream_09',
'BBC Red Button 10' => 'sport_stream_10',
'BBC Red Button 11' => 'sport_stream_11',
'BBC Red Button 12' => 'sport_stream_12',
'BBC Red Button 13' => 'sport_stream_13',
'BBC Red Button 14' => 'sport_stream_14',
'BBC Red Button 15' => 'sport_stream_15',
'BBC Red Button 16' => 'sport_stream_16',
'BBC Red Button 17' => 'sport_stream_17',
'BBC Red Button 18' => 'sport_stream_18',
'BBC Red Button 19' => 'sport_stream_19',
'BBC Red Button 20' => 'sport_stream_20',
'BBC Red Button 21' => 'sport_stream_21',
'BBC Red Button 22' => 'sport_stream_22',
'BBC Red Button 23' => 'sport_stream_23',
'BBC Red Button 24' => 'sport_stream_24',
'BBC Red Button 01b' => 'sport_stream_01b',
'BBC Red Button 02b' => 'sport_stream_02b',
'BBC Red Button 03b' => 'sport_stream_03b',
'BBC Red Button 04b' => 'sport_stream_04b',
'BBC Red Button 05b' => 'sport_stream_05b',
'BBC Red Button 06b' => 'sport_stream_06b',
'BBC Red Button 07b' => 'sport_stream_07b',
'BBC Red Button 08b' => 'sport_stream_08b',
'BBC Red Button 09b' => 'sport_stream_09b',
'BBC Red Button 10b' => 'sport_stream_10b',
'BBC Red Button 11b' => 'sport_stream_11b',
'BBC Red Button 12b' => 'sport_stream_12b',
'BBC Red Button 13b' => 'sport_stream_13b',
'BBC Red Button 14b' => 'sport_stream_14b',
'BBC Red Button 15b' => 'sport_stream_15b',
'BBC Red Button 16b' => 'sport_stream_16b',
'BBC Red Button 17b' => 'sport_stream_17b',
'BBC Red Button 18b' => 'sport_stream_18b',
'BBC Red Button 19b' => 'sport_stream_19b',
'BBC Red Button 20b' => 'sport_stream_20b',
'BBC Red Button 21b' => 'sport_stream_21b',
'BBC Red Button 22b' => 'sport_stream_22b',
'BBC Red Button 23b' => 'sport_stream_23b',
'BBC Red Button 24b' => 'sport_stream_24b',
'BBC Scotland Stream 01' => 'scotland_stream_01',
'BBC Scotland Stream 02' => 'scotland_stream_02',
'BBC Scotland Stream 03' => 'scotland_stream_03',
'BBC Scotland Stream 04' => 'scotland_stream_04',
}
},
'radio' => {
'national' => {
'BBC Asian Network' => 'bbc_asian_network',
'BBC Radio 1' => 'bbc_radio_one',
'BBC Radio 1Xtra' => 'bbc_1xtra',
'BBC Radio 2' => 'bbc_radio_two',
'BBC Radio 3' => 'bbc_radio_three',
'BBC Radio 4 Extra' => 'bbc_radio_four_extra',
'BBC Radio 4 FM' => 'bbc_radio_fourfm',
'BBC Radio 4 LW' => 'bbc_radio_fourlw',
'BBC Radio 5 live sports extra' => 'bbc_radio_five_live_sports_extra',
'BBC Radio 5 live' => 'bbc_radio_five_live',
'BBC Radio 6 Music' => 'bbc_6music',
'BBC World Service' => 'bbc_world_service',
},
'regional' => {
'BBC Radio Cymru' => 'bbc_radio_cymru',
'BBC Radio Foyle' => 'bbc_radio_foyle',
'BBC Radio Nan Gaidheal' => 'bbc_radio_nan_gaidheal',
'BBC Radio Scotland FM' => 'bbc_radio_scotland_fm',
'BBC Radio Scotland MW' => 'bbc_radio_scotland_mw',
'BBC Radio Ulster' => 'bbc_radio_ulster',
'BBC Radio Wales' => 'bbc_radio_wales_fm',
},
'local' => {
'BBC Coventry & Warwickshire' => 'bbc_radio_coventry_warwickshire',
'BBC Essex' => 'bbc_radio_essex',
'BBC Hereford & Worcester' => 'bbc_radio_hereford_worcester',
'BBC Newcastle' => 'bbc_radio_newcastle',
'BBC Radio Berkshire' => 'bbc_radio_berkshire',
'BBC Radio Bristol' => 'bbc_radio_bristol',
'BBC Radio Cambridgeshire' => 'bbc_radio_cambridge',
'BBC Radio Cornwall' => 'bbc_radio_cornwall',
'BBC Radio Cumbria' => 'bbc_radio_cumbria',
'BBC Radio Derby' => 'bbc_radio_derby',
'BBC Radio Devon' => 'bbc_radio_devon',
'BBC Radio Gloucestershire' => 'bbc_radio_gloucestershire',
'BBC Radio Guernsey' => 'bbc_radio_guernsey',
'BBC Radio Humberside' => 'bbc_radio_humberside',
'BBC Radio Jersey' => 'bbc_radio_jersey',
'BBC Radio Kent' => 'bbc_radio_kent',
'BBC Radio Lancashire' => 'bbc_radio_lancashire',
'BBC Radio Leeds' => 'bbc_radio_leeds',
'BBC Radio Leicester' => 'bbc_radio_leicester',
'BBC Radio Lincolnshire' => 'bbc_radio_lincolnshire',
'BBC Radio London' => 'bbc_london',
'BBC Radio Manchester' => 'bbc_radio_manchester',
'BBC Radio Merseyside' => 'bbc_radio_merseyside',
'BBC Radio Norfolk' => 'bbc_radio_norfolk',
'BBC Radio Northampton' => 'bbc_radio_northampton',
'BBC Radio Nottingham' => 'bbc_radio_nottingham',
'BBC Radio Oxford' => 'bbc_radio_oxford',
'BBC Radio Sheffield' => 'bbc_radio_sheffield',
'BBC Radio Shropshire' => 'bbc_radio_shropshire',
'BBC Radio Solent' => 'bbc_radio_solent',
'BBC Radio Stoke' => 'bbc_radio_stoke',
'BBC Radio Suffolk' => 'bbc_radio_suffolk',
'BBC Radio York' => 'bbc_radio_york',
'BBC Somerset' => 'bbc_radio_somerset_sound',
'BBC Surrey' => 'bbc_radio_surrey',
'BBC Sussex' => 'bbc_radio_sussex',
'BBC Tees' => 'bbc_tees',
'BBC Three Counties Radio' => 'bbc_three_counties_radio',
'BBC Wiltshire' => 'bbc_radio_wiltshire',
'BBC WM 95.6' => 'bbc_wm',
}
}
};
# munge channel name for desired sorting
sub channel_name_sortable {
my $s = shift;
my $type = shift;
if ( $type eq "tv" ) {
$s =~ s/^BBC Radio/X/i;
} else {
$s =~ s/^BBC( Radio)?//i;
}
$s =~ s/One/1/i;
$s =~ s/Two/2/i;
$s =~ s/Extra/Xtra/i;
$s =~ s/^(BBC Red Button \d{2}b)$/Y$1/i;
$s =~ s/^(BBC Scotland Stream \d{2})$/Z$1/i;
return $s;
}
# create ordered lists of channel names for display
my $channel_names;
for my $type ( keys %{$channels} ) {
for my $group ( keys %{$channels->{$type}} ) {
for my $name ( sort { channel_name_sortable($a, $type) cmp channel_name_sortable($b, $type) } keys %{$channels->{$type}->{$group}} ) {
push @{$channel_names->{$type}->{$group}}, $name;
}
}
}
# get playlists from mediaselector API
sub mediaselector_playlists {
my $ua = shift;
my $type = shift;
my $channel = shift;
my $playlists;
# lowest to highest quality
my @mediasets;
if ( $type eq "tv" ) {
@mediasets = ( "apple-ipad-hls", "iptv-all" );
} else {
@mediasets = ( "apple-iphone4-ipad-hls-3g", "apple-ipad-hls");
}
my %seen;
for my $i ( 0 .. $#mediasets ){
my $mediaset = $mediasets[$i];
my $mediaselector = "http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/mediaset/$mediaset/vpid/$channel/format/json";
app->log->debug("mediaselector: $mediaselector");
my $tx = $ua->get($mediaselector);
my $res = $tx->success;
if ( ! $res ) {
my $err = $tx->error;
if ( $err->{code} ) {
app->log->error("code: $err->{code} response: $err->{message}");
} else {
app->log->error("connection error: $err->{message}");
}
next;
}
my $json = decode_json($res->body);
for my $media ( @{$json->{media}} ) {
my $media_playlists;
for my $conn ( @{$media->{connection}} ) {
# skip other stream types
next unless $conn->{transferFormat} eq "hls";
# skip https URLs
next if $conn->{protocol} eq "https";
my $url = $conn->{href};
# skip duplicates
next if $seen{$url};
$seen{$url} = 1;
my $supplier = $conn->{supplier} =~ /^ll/i ? "Limelight" : $conn->{supplier} =~ /^ak/i ? "Akamai" : ucfirst($conn->{supplier} );
my $priority = $conn->{priority};
my $availability;
if ( $type eq "tv" ) {
$availability = "UK Only";
} else {
$availability = ( ( $url =~ /[\/-]uk[\/-]/ && $channel ne "bbc_world_service" ) || $channel eq "bbc_radio_five_live_sports_extra" ) ? "UK Only" : "UK & Intl";
}
my $key = sprintf("%02d-%03d", $i+1, $priority);
my $playlist = {
key => $key,
type => $type,
mediaset => $mediaset,
target => $mediaset eq "iptv-all" ? "IPTV" : "Mobile",
availability => $availability,
supplier => $supplier,
priority => $priority,
bitrate => $type eq "tv" ? 0 : $media->{bitrate},
url => $url
};
$media_playlists->{$key} = $playlist;
}
@{$playlists}{keys %{$media_playlists}} = values %{$media_playlists};
# break after first media descriptor with HLS
last if keys %{$media_playlists} != 0;
}
}
return $playlists;
}
# generate tv playlists without mediaselector API
sub tv_playlists {
my $ua = shift;
my $channel = shift;
my $playlists;
# lowest to highest quality
# my @mediasets = ( "hls_mobile_3g_main", "hls_tablet_rw" );
my @mediasets = ( "hls_tablet" );
# HD only for national channels (except BBC Parliament) and regional BBC One
if ( ( grep(/^$channel$/, values %{$channels->{tv}->{national}}) && $channel !~ /bbc_parliament/ ) ||
( grep(/^$channel$/, values %{$channels->{tv}->{regional}}) && $channel =~ /bbc_one/ ) ||
( grep(/^$channel$/, values %{$channels->{tv}->{'red button'}}) ) ) {
push @mediasets, "abr_hdtv";
} else {
push @mediasets, "abr_tv";
}
my $broadcast = "simulcast";
if ( grep(/^$channel$/, values %{$channels->{tv}->{'red button'}}) ) {
$broadcast = "webcast";
}
# randomise CDNs
#my @rand_cdns = ( "ak", "llnw" );
#my @cdns = $rand_cdns[rand @rand_cdns];
#push @cdns, $cdns[0] eq "ak" ? "llnw" : "ak";
# reverse alphabetical
my @cdns = ( "llnw", "ak" );
for my $i ( 0 .. $#mediasets ) {
my $mediaset = $mediasets[$i];
for my $j ( 0 .. $#cdns ) {
my $cdn = $cdns[$j];
my $supplier = $cdn eq "ak" ? "Akamai" : "Limelight";
my $priority = $j + 1;
my $key = sprintf("%02d-%02d", $i+1, $priority);
my $playlist = {
key => $key,
type => 'tv',
mediaset => $mediaset,
target => $mediaset =~ /abr/ ? "IPTV" : "Mobile",
availability => "UK Only",
supplier => $supplier,
priority => $priority,
bitrate => 0,
url => "http://a.files.bbci.co.uk/media/live/manifesto/audio_video/${broadcast}/hls/uk/${mediaset}/${cdn}/${channel}.m3u8"
};
$playlists->{$key} = $playlist;
}
}
return $playlists;
}
# generate radio playlists without mediaselector API
sub radio_playlists {
my $ua = shift;
my $channel = shift;
my $playlists;
my $bitrates = {
'sbr_vlow' => 48,
'sbr_low' => 96,
'sbr_med' => 128,
'sbr_high' => 320,
};
# non-UK before UK
my @mediasets = ( "nonuk", "uk" );
# randomise CDNs
#my @rand_cdns = ( "ak", "llnw" );
#my @cdns = $rand_cdns[rand @rand_cdns];
#push @cdns, $cdns[0] eq "ak" ? "llnw" : "ak";
# reverse alphabetical
my @cdns = ( "llnw", "ak" );
for my $i ( 0 .. $#mediasets ) {
my $mediaset = $mediasets[$i];
# no non-UK streams for bbc_radio_five_live_sports_extra
next if $channel eq "bbc_radio_five_live_sports_extra" and $mediaset eq "nonuk";
# no UK streams for bbc_world_service
next if $channel eq "bbc_world_service" and $mediaset eq "uk";
my @sbrs;
# lowest to highest quality
if ( $mediaset eq "uk" ) {
@sbrs = ( 'sbr_vlow', 'sbr_low', 'sbr_med', 'sbr_high' );
} else {
@sbrs = ( 'sbr_vlow', 'sbr_low' );
}
for my $j ( 0 .. $#sbrs ){
my $sbr = $sbrs[$j];
for my $k ( 0 .. $#cdns ) {
my $cdn = $cdns[$k];
my $supplier = $cdn eq "ak" ? "Akamai" : "Limelight";
my $priority = $k + 1;
my $availability = ( ( $mediaset eq "uk" && $channel ne "bbc_world_service" ) || $channel eq "bbc_radio_five_live_sports_extra" ) ? "UK Only" : "UK & Intl";
my $key = sprintf("%02d-%02d-%02d", $i+1, $j+1, $priority);
my $playlist = {
key => $key,
type => 'radio',
mediaset => $mediaset,
target => "Any",
availability => $availability,
supplier => $supplier,
priority => $priority,
bitrate => $bitrates->{$sbr},
url => "http://a.files.bbci.co.uk/media/live/manifesto/audio/simulcast/hls/${mediaset}/${sbr}/${cdn}/${channel}.m3u8"
};
$playlists->{$key} = $playlist;
}
}
}
return $playlists;
}
# generate playlists without mediaselector API
sub generate_playlists {
my $ua = shift;
my $type = shift;
my $channel = shift;
if ( $type eq "tv" ) {
return tv_playlists($ua, $channel);
} else {
return radio_playlists($ua, $channel);
}
}
# get streams in single playlist
sub playlist_streams {
my $ua = shift;
my $playlist = shift;
my $type = $playlist->{type};
my $streams;
app->log->debug("playlist: $playlist->{url}");
my $tx = $ua->get($playlist->{url});
my $res = $tx->success;
if ( ! $res ) {
my $err = $tx->error;
if ( $err->{code} ) {
app->log->error("code: $err->{code} response: $err->{message}");
} else {
app->log->error("connection error: $err->{message}");
}
return undef;
}
my @lines = m3u_parser($res->body);
my %seen;
for my $i ( 0 .. $#lines ) {
my $line = $lines[$i];
if ( $line->{type} eq 'directive' && $line->{tag} eq 'EXT-X-STREAM-INF' ) {
my ($bitrate, $width, $height) = (0, 0, 0);
if ( $type eq "tv" ) {
($bitrate, $width, $height) = $line->{value} =~ /BANDWIDTH=(\d+).*?RESOLUTION=(\d+)x(\d+)/i;
} else {
($bitrate) = $line->{value} =~ /BANDWIDTH=(\d+)/i;
}
$bitrate = int($bitrate / 1000);
$line = $lines[++$i];
if ( $line->{type} eq 'item' ) {
my $url = $line->{value};
# skip duplicates
next if $seen{$url};
$seen{$url} = 1;
my ($audio_bitrate, $video_bitrate) = (0, 0);
if ( $type eq "tv" ) {
($audio_bitrate, $video_bitrate) = $url =~ /%3d(\d+)-video%3d(\d+)/i;
} else {
($audio_bitrate) = $url =~ /-audio%3d(\d+)/i;
}
$audio_bitrate = int($audio_bitrate / 1000);
$video_bitrate = int($video_bitrate / 1000);
my $key;
if ( $type eq "tv" ) {
$key = sprintf("%04d-%05d-%03d", $height, $bitrate, $playlist->{priority});
} else {
$key = $playlist->{key};
}
my $stream = {
key => $key,
type => $playlist->{type},
mediaset => $playlist->{mediaset},
supplier => $playlist->{supplier},
priority => $playlist->{priority},
target => $playlist->{target},
availability => $playlist->{availability},
bitrate => $bitrate,
width => $width,
height => $height,
audio_bitrate => $audio_bitrate,
video_bitrate => $video_bitrate,
frame_rate => $type eq "tv" ? $bitrate > 3000 ? 50 : 25 : 0,
url => $url
};
$streams->{$key} = $stream;
app->log->debug("stream: $stream->{url}");
}
}
}
return $streams;
}
# get streams in multiple playlists
sub playlists_streams {
my $ua = shift;
my $playlists = shift;
my $streams;
for my $key ( keys %{$playlists} ) {
my $playlist = $playlists->{$key};
my $playlist_streams = playlist_streams($ua, $playlist);
@{$streams}{keys %{$playlist_streams}} = values %{$playlist_streams};
}
return $streams;
}
# routes
get '/' => {template => 'index'};
get '/channels/:medsel/:type' => sub {
my $c = shift;
my $type = $c->param('type');
$c->stash(channels => $channels->{$type});
$c->stash(channel_names => $channel_names->{$type});
$c->render(template => 'channels');
};
get '/playlists/:medsel/:type/:channel/:name' => sub {
my $c = shift;
my $type = $c->param('type');
my $channel = $c->param('channel');
my $playlists;
if ( $c->param('medsel') ) {
$playlists = mediaselector_playlists($c->ua, $type, $channel);
} else {
$playlists = generate_playlists($c->ua, $type, $channel);
}
my $streams = playlists_streams($c->ua, $playlists);
$c->stash(playlists => $playlists, streams => $streams);
$c->render(template => 'playlists', variant => $type);
};
app->start;
__DATA__
@@ layouts/bbclive.html.ep
<!DOCTYPE html>
<html>
<head><title>BBC Live</title></head>
<body><%= content %></body>
</html>
@@ index.html.ep
% layout 'bbclive';
<h1>BBC Live</h1>
<h2>(Generated Playlists)</h2>
<a href="/channels/0/tv"><h2>TV</h2></a>
<a href="/channels/0/radio"><h2>Radio</h2></a>
<h2>(Mediaselector API)</h2>
<a href="/channels/1/tv"><h2>TV</h2></a>
<a href="/channels/1/radio"><h2>Radio</h2></a>
@@ channels.html.ep
% layout 'bbclive';
% use Mojo::Util qw(url_escape);
<h1><a href="/">BBC Live</a> > <%= $type eq "tv" ? "TV" : "Radio" %></h1>
<h2>(<%= $medsel ? "Mediaselector API" : "Generated Playlists" %>)</h2>
% for my $group ( 'national', 'regional', 'local', 'red button' ) {
<h2><%= ucfirst $group %><br/></h2>
% for my $name ( @{$channel_names->{$group}} ) {
<a href="/playlists/<%= $medsel %>/<%= $type %>/<%= $channels->{$group}->{$name} %>/<%= url_escape $name %>"><%= $name %></a><br/>
% }
% }
@@ playlists.html+tv.ep
% layout 'bbclive';
% my $header = begin
% my $title = shift;
<tr><td colspan="11"><h2><%= $title %></h2></td></tr>
<tr valign="top">
<th>Geographic<br/>Availability</th>
<th>Target<br/>Platform</th>
<th>Video<br/>Size</th>
<th>Frame<br/>Rate</th>
<th>Video<br/>Quality</th>
<th>Video<br/>Format</th>
<th>Audio<br/>Quality</th>
<th>Audio<br/>Format</th>
<th>Stream<br/>Type</th>
<th>CDN<br/>Supplier</th>
<th></th>
</tr>
% end
% my $row = begin
% my $x = shift;
<tr>
<td><%= $x->{availability} %></td>
<td><%= $x->{target} %></td>
<td><%= $x->{height} ? $x->{width}."x".$x->{height} : "Adaptive" %></td>
<td><%= $x->{frame_rate} ? $x->{frame_rate}."fps" : "Adaptive" %></td>
<td><%= $x->{video_bitrate} ? $x->{video_bitrate}."k" : "Adaptive" %></td>
<td>H.264</td>
<td><%= $x->{audio_bitrate} ? $x->{audio_bitrate}."k" : "Adaptive" %></td>
<td>AAC</td>
<td>HLS</td>
<td><%= $x->{supplier} %></td>
<td><a target="_blank" href="<%= $x->{url} %>">Play</a></td>
</tr>
% end
<h1><a href="/">BBC Live</a> > <a href="/channels/<%= $medsel %>/tv">TV</a> > <%= $name %></h1>
<h2>(<%= $medsel ? "Mediaselector API" : "Generated Playlists" %>)</h2>
<table cellspacing="3">
%= $header->('Variant Playlists')
% for my $key ( reverse sort keys %{$playlists} ) {
%= $row->($playlists->{$key})
% }
%= $header->('Individual Streams')
% for my $key ( reverse sort keys %{$streams} ) {
%= $row->($streams->{$key})
% }
</table>
@@ playlists.html+radio.ep
% layout 'bbclive';
% my $header = begin
% my $title = shift;
<tr><td colspan="6"><h2><%= $title %></h2></td></tr>
<tr valign="top">
<th>Geographic<br/>Availability</th>
<th>Audio<br/>Quality</th>
<th>Audio<br/>Format</th>
<th>Stream<br/>Type</th>
<th>CDN<br/>Supplier</th>
<th></th>
</tr>
% end
% my $row = begin
% my $x = shift;
<tr>
<td><%= $x->{availability} %></td>
<td><%= $x->{audio_bitrate} || $x->{bitrate} %>k</td>
<td>AAC</td>
<td>HLS</td>
<td><%= $x->{supplier} %></td>
<td><a target="_blank" href="<%= $x->{url} %>">Play</a></td>
</tr>
% end
<h1><a href="/">BBC Live</a> > <a href="/channels/<%= $medsel %>/radio">Radio</a> > <%= $name %></h1>
<h2>(<%= $medsel ? "Mediaselector API" : "Generated Playlists" %>)</h2>
<table cellspacing="3">
%= $header->('Variant Playlists')
% for my $key ( reverse sort keys %{$playlists} ) {
%= $row->($playlists->{$key})
% }
%= $header->('Individual Streams')
% for my $key ( reverse sort keys %{$streams} ) {
%= $row->($streams->{$key})
% }
</table>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment