Skip to content

Commit

Permalink
headers for remote streams (including volatile)
Browse files Browse the repository at this point in the history
continue

continue

continue

headers for remote streams (including volatile)

continue

continue

continue

continue

continue

continue
  • Loading branch information
philippe44 committed Jun 18, 2020
1 parent 1bac517 commit 8884c60
Show file tree
Hide file tree
Showing 11 changed files with 356 additions and 60 deletions.
10 changes: 9 additions & 1 deletion Slim/Formats/AIFF.pm
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ sub getTag {
return $tags;
}

sub volatileInitialAudioBlock { 1 }

sub getInitialAudioBlock {
my ($class, $fh, $track) = @_;
my ($class, $fh, $track, $time) = @_;
my $length = $track->audio_offset() || return undef;

open(my $localFh, '<&=', $fh);
Expand All @@ -92,6 +94,12 @@ sub getInitialAudioBlock {
seek($localFh, 0, 0);
close($localFh);

# adjust header size according to seek position
my $trim = int($time * $track->bitrate / 8);
$trim -= $trim % ($track->block_alignment || 1);
substr($buffer, $length - 3*4, 4, pack('N', $track->audio_size - $trim));
substr($buffer, 4, 4, pack('N', unpack('N', substr($buffer, 4, 4) - $trim)));

return $buffer;
}

Expand Down
2 changes: 2 additions & 0 deletions Slim/Formats/Movie.pm
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ sub _doTagMapping {
}
}

sub volatileInitialAudioBlock { 1 }

sub getInitialAudioBlock {
my ($class, $fh, $track, $time) = @_;

Expand Down
9 changes: 8 additions & 1 deletion Slim/Formats/Wav.pm
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ sub getTag {
return $tags;
}

sub volatileInitialAudioBlock { 1 }

sub getInitialAudioBlock {
my ($class, $fh, $track) = @_;
my ($class, $fh, $track, $time) = @_;
my $length = $track->audio_offset() || return undef;

open(my $localFh, '<&=', $fh);
Expand All @@ -69,6 +71,11 @@ sub getInitialAudioBlock {
read ($localFh, my $buffer, $length);
seek($localFh, 0, 0);
close($localFh);

# adjust header size according to seek position
my $trim = int($time * $track->bitrate / 8);
$trim -= $trim % ($track->block_alignment || 1);
substr($buffer, $length - 4, 4, pack('V', $track->audio_size - $trim));

return $buffer;
}
Expand Down
14 changes: 6 additions & 8 deletions Slim/Player/Protocols/File.pm
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ sub open {
$streamLength = $song->streamLength();
$seekoffset = $seekdata->{restartOffset};
} elsif ($seekdata->{sourceStreamOffset}) { # used for seeking
$seekoffset = $seekdata->{sourceStreamOffset} + $seekdata->{sourceHeaderOffset};
$seekoffset = $seekdata->{sourceStreamOffset};
$streamLength -= $seekdata->{sourceStreamOffset} - $offset;
} else {
$seekoffset = $offset; # normal case
Expand All @@ -151,7 +151,7 @@ sub open {
${*$sock}{'streamFormat'} = $args->{'transcoder'}->{'streamformat'};

if ( $seekoffset

&& !$song->stripHeader
# We do not need to worry about an initialAudioBlock when we are restarting
# as getSeekDataByPosition() will not have allowed a restart within the
# initialAudioBlock.
Expand All @@ -172,18 +172,16 @@ sub open {
main::DEBUGLOG && $log->debug("Got initial audio block of size $length");
if ($seekoffset <= $length) {
# Might as well just play from the start normally
$offset = $seekoffset = 0;
$streamLength = $size + $offset;
$offset = $seekoffset = 0;
} else {
${*$sock}{'initialAudioBlockRemaining'} = $length;
${*$sock}{'initialAudioBlockRef'} = \($song->initialAudioBlock());
$streamLength += $length;
}

# For MP4 files, we can't cache the audio block because it's different each time
if ($streamClass eq 'Slim::Formats::Movie') {
$song->initialAudioBlock(undef);
}
# For some files, we can't cache the audio block because it's different each time
$song->initialAudioBlock(undef) if $streamClass->can('volatileInitialAudioBlock') && $streamClass->volatileInitialAudioBlock($track);
}
}

Expand All @@ -192,7 +190,7 @@ sub open {
if (!defined(sysseek($sock, $seekoffset, 0))) {
logError("could not seek to $seekoffset for $filepath: $!");
} else {
$client->songBytes($seekoffset - ${*$sock}{'initialAudioBlockRemaining'});
$client->songBytes($seekoffset - ($song->stripHeader ? $song->offset : ${*$sock}{'initialAudioBlockRemaining'}));
${*$sock}{'position'} = $seekoffset;
if ($seekoffset > $offset && $seekdata && $seekdata->{'timeOffset'}) {
$song->startOffset($seekdata->{'timeOffset'});
Expand Down
116 changes: 100 additions & 16 deletions Slim/Player/Protocols/HTTP.pm
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,39 @@ sub new {
return $self;
}

sub request {
my $self = shift;
my $args = shift;
my $song = $args->{'song'};

# header is optional even when proxied, but define it empty for requestString
if (!defined $song->track->dynamic_header || $song->stripHeader) {
$song->initialAudioBlock('');
return $self->SUPER::request($args);
}

# obtain initial audio block if missing and adjust seekdata then
if ($song->track->build_header && !defined $song->initialAudioBlock) {
my $info = $song->track->build_header->($song->track, $song->seekdata ? $song->seekdata->{timeOffset} : 0);
$song->initialAudioBlock($info->{seek_header});
$song->seekdata->{sourceStreamOffset} = $info->{seek_offset} if $info->{seek_offset};
}

# all set for opening the HTTP object
$self = $self->SUPER::request($args);
return unless $self;

my $length = length($song->initialAudioBlock);
${*$self}{'initialAudioBlockRemaining'} = $length;
${*$self}{'initialAudioBlockRef'} = \($song->initialAudioBlock);
main::DEBUGLOG && $log->debug("Got initial audio block of size $length");

# dynamic headers need to be re-calculated every time
$song->initialAudioBlock(undef) if $song->track->dynamic_header;

return $self;
}

sub isRemote { 1 }

sub readMetaData {
Expand Down Expand Up @@ -130,6 +163,7 @@ sub parseMetadata {
my $url = Slim::Player::Playlist::url(
$client, Slim::Player::Source::streamingSongIndex($client)
);


# See if there is a parser for this stream
my $parser = Slim::Formats::RemoteMetadata->getParserFor( $url );
Expand Down Expand Up @@ -286,9 +320,51 @@ sub canDirectStream {
return $url;
}

# TODO: what happens is this method is overloaded in a sub-class that does not call its parents method
sub canDirectStreamSong {
my ( $class, $client, $song ) = @_;

# can't go direct if we are synced or proxy is set by user
my $direct = $class->canDirectStream( $client, $song->streamUrl(), $class->getFormatForURL() );
return 0 unless $direct;

# no header or stripHeader flag has precedence
return $direct if $song->stripHeader || !defined $song->track->dynamic_header;

# if header always need to be re-calculated, we can't go direct anyway
if ($song->track->dynamic_header == 2 || $song->seekdata) {
main::INFOLOG && $directlog->info("Need to add header, cannot stream direct");
return 0;
}

return $direct;
}

sub sysread {
my $self = $_[0];
my $chunkSize = $_[2];

# stitch header if any
if (my $length = ${*$self}{'initialAudioBlockRemaining'}) {

my $chunkLength = $length;
my $chunkref;

main::DEBUGLOG && $log->debug("getting initial audio block of size $length");

if ($length > $chunkSize || $length < length(${${*$self}{'initialAudioBlockRef'}})) {
$chunkLength = $length > $chunkSize ? $chunkSize : $length;
my $chunk = substr(${${*$self}{'initialAudioBlockRef'}}, -$length, $chunkLength);
$chunkref = \$chunk;
${*$self}{'initialAudioBlockRemaining'} = ($length - $chunkLength);
} else {
${*$self}{'initialAudioBlockRemaining'} = 0;
$chunkref = ${*$self}{'initialAudioBlockRef'};
}

$_[1] = $$chunkref;
return $chunkLength;
}

my $metaInterval = ${*$self}{'metaInterval'};
my $metaPointer = ${*$self}{'metaPointer'};
Expand Down Expand Up @@ -588,20 +664,25 @@ sub requestString {
$request .= $CRLF . "Authorization: Basic " . MIME::Base64::encode_base64($user . ":" . $password,'');
}

$client->songBytes(0) if $client;

# If seeking, add Range header
if ($client && $seekdata) {
$request .= $CRLF . 'Range: bytes=' . int( ($seekdata->{sourceStreamOffset} || 0) + ($seekdata->{restartOffset} || 0) + ($seekdata->{sourceHeaderOffset} || 0)) . '-';
$request .= $seekdata->{sourceStreamLength} + $seekdata->{sourceHeaderOffset} - 1 if $seekdata->{sourceStreamLength};
if ($client) {
my $song = $client->streamingSong;
$client->songBytes(0);

my $first = $seekdata->{restartOffset} || int( $seekdata->{sourceStreamOffset} );
$first ||= $song->track->audio_offset if $song->stripHeader || defined $song->initialAudioBlock;

if ($first) {
$request .= $CRLF . 'Range: bytes=' . ($first || 0) . '-' . ($song->track->audio_size || '');

if (defined $seekdata->{timeOffset}) {
# Fix progress bar
$client->playingSong()->startOffset($seekdata->{timeOffset});
$client->master()->remoteStreamStartTime( Time::HiRes::time() - $seekdata->{timeOffset} );
}

if (defined $seekdata->{timeOffset}) {
# Fix progress bar
$client->playingSong()->startOffset($seekdata->{timeOffset});
$client->master()->remoteStreamStartTime( Time::HiRes::time() - $seekdata->{timeOffset} );
$client->songBytes( $first - ($song->stripHeader ? $song->track->audio_offset : 0) );
}

$client->songBytes(int( $seekdata->{sourceStreamOffset} ));
}

# Send additional information if we're POSTing
Expand Down Expand Up @@ -837,10 +918,10 @@ sub getSeekData {

my $offset = int (( ( $bitrate * 1000 ) / 8 ) * $newtime);
$offset -= $offset % ($song->track->block_alignment || 1);
$offset += $song->seekdata->{'streamHeaderOffset'} || 0;


# this might be re-calculated by open() if direct streaming is disabled
return {
sourceStreamOffset => $offset,
sourceStreamOffset => $offset + $song->track->audio_offset,
timeOffset => $newtime,
};
}
Expand All @@ -849,8 +930,11 @@ sub getSeekDataByPosition {
my ($class, $client, $song, $bytesReceived) = @_;

my $seekdata = $song->seekdata() || {};

return {%$seekdata, restartOffset => $bytesReceived};

my $position = int($seekdata->{'sourceStreamOffset'}) || 0;
$position ||= $song->track->audio_offset if defined $song->initialAudioBlock;

return {%$seekdata, restartOffset => $position + $bytesReceived - $song->initialAudioBlock};
}

1;
Expand Down
22 changes: 22 additions & 0 deletions Slim/Player/Protocols/HTTPS.pm
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,28 @@ sub canDirectStream {
sub sysread {
my $self = $_[0];
my $chunkSize = $_[2];

# stitch header if any
if (my $length = ${*$self}{'initialAudioBlockRemaining'}) {

my $chunkLength = $length;
my $chunkref;

main::DEBUGLOG && $log->debug("getting initial audio block of size $length");

if ($length > $chunkSize || $length < length(${${*$self}{'initialAudioBlockRef'}})) {
$chunkLength = $length > $chunkSize ? $chunkSize : $length;
my $chunk = substr(${${*$self}{'initialAudioBlockRef'}}, -$length, $chunkLength);
$chunkref = \$chunk;
${*$self}{'initialAudioBlockRemaining'} = ($length - $chunkLength);
} else {
${*$self}{'initialAudioBlockRemaining'} = 0;
$chunkref = ${*$self}{'initialAudioBlockRef'};
}

$_[1] = $$chunkref;
return $chunkLength;
}

my $metaInterval = ${*$self}{'metaInterval'};
my $metaPointer = ${*$self}{'metaPointer'};
Expand Down
16 changes: 3 additions & 13 deletions Slim/Player/Song.pm
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ my @_playlistCloneAttributes = qw(
startOffset streamLength
seekdata initialAudioBlock
_canSeek _canSeekError
stripHeader
_duration _bitrate _streambitrate _streamFormat
_transcoded directstream
Expand Down Expand Up @@ -445,19 +446,8 @@ sub open {
};
}

# now can set seekdata support information
$seekdata->{'sourceStreamLength'} = $track->audio_size;

if ($transcoder->{'stripHeader'}) {
# Set initialAudioBlock to 0 (not undef) so that File does not re-send it
$self->initialAudioBlock(0);
# Tell File and HTTP to strip header from source
$seekdata->{'sourceHeaderOffset'} = $track->audio_offset || 0,
main::INFOLOG && $log->info( "stripping header of ", $seekdata->{'sourceStreamOffset'}, " / ", $seekdata->{'sourceStreamLength'});
}

# update seekdata (might be only support information)
$self->seekdata($seekdata);
# don't modify $song in getConverterCommand2
$self->stripHeader($transcoder->{'stripHeader'});

# TODO work this out for each player in the sync-group
my $directUrl;
Expand Down
2 changes: 0 additions & 2 deletions Slim/Player/StreamingController.pm
Original file line number Diff line number Diff line change
Expand Up @@ -1225,9 +1225,7 @@ sub _Stream { # play -> Buffering, Streaming
$self->{'songStreamController'} = undef;
}

# create song and re-acquire seekdata as open() can modify/created it
my ($songStreamController, @error) = $song->open($seekdata);
$seekdata = $song->seekdata;

if (!$songStreamController) {
_errorOpening($self, $song->currentTrack()->url, @error);
Expand Down
21 changes: 6 additions & 15 deletions Slim/Player/TranscodingHelper.pm
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ sub getConvertCommand2 {
push @profiles, "$type-$checkFormat-transcode-*";
}
}

# Test each profile in turn
PROFILE: foreach my $profile (@profiles) {
my $command = checkBin($profile);
Expand Down Expand Up @@ -396,15 +396,6 @@ sub getConvertCommand2 {

my $streamformat = (split (/-/, $profile))[1];

# aif/wav oddity #1 (for remote seek)
# when seeking a remote track, can't re-send the header, it's lost
if ($type =~ /(wav|aif)/ && !($streamformat eq 'pcm' && $command eq '-' )
&& $track->remote && $song->seekdata) {
main::DEBUGLOG && $log->is_debug
&& $log->debug("Rejecting $command because header is unavailable when seeking");
next PROFILE;
}

$transcoder = {
command => $command,
profile => $profile,
Expand All @@ -419,14 +410,14 @@ sub getConvertCommand2 {
player => $player || 'undefined',
channels => $track->channels() || 2,
outputChannels => $clientprefs ? ($clientprefs->get('outputChannels') || 2) : 2,
# aif/wav oddity #2 (for '-' rule)
# aif/wav oddity (for '-' rule)
# aif - aif: any SB that does not support wav requires aif header stripping
# wav/aif - pcm: force header stripping
stripHeader => $caps->{'H'} || ($command eq "-" &&
(($type =~ /(wav|aif)/ && $streamformat eq 'pcm') ||
($type eq 'aif' && $streamformat eq 'aif' && !grep {$_ eq 'wav'} @supportedformats))),
};

(($type =~ /(wav|aif)/ && $streamformat eq 'pcm') ||
($type eq 'aif' && $streamformat eq 'aif' && !grep {$_ eq 'wav'} @supportedformats))),
};
# Check for optional profiles
my $wanted = 0;
my @got = ();
Expand Down
1 change: 1 addition & 0 deletions Slim/Schema/RemoteTrack.pm
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ my @allAttributes = (qw(
title titlesort titlesearch album tracknum
timestamp filesize disc audio audio_size audio_offset year
header_stash build_header dynamic_header
cover vbr_scale samplerate samplesize channels block_alignment endian
bpm tagversion drm musicmagic_mixable
musicbrainz_id lossless lyrics replay_gain replay_peak extid
Expand Down

0 comments on commit 8884c60

Please sign in to comment.