Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions plugin/ProtocolHandler.pm
Original file line number Diff line number Diff line change
Expand Up @@ -346,11 +346,16 @@ sub sysread_URL {
}

# process all available data
$handler->getAudio(\$v->{outBuf}) if $handler->bufferLength;
my $audioRes = $handler->bufferLength ? $handler->getAudio(\$v->{outBuf}) : undef;

if ( my $bytes = min(length $v->{outBuf}, $_[2]) ) {
$_[1] = substr($v->{outBuf}, 0, $bytes, '');
return $bytes;
} elsif ( defined $audioRes && $audioRes eq Plugins::YouTube::WebM::WEBM_ERROR() ) {
# parser gave up. end the stream so LMS skips to next track
# instead of stalling on an empty buffer
$log->warn("parser error, ending stream early");
return 0;
} elsif ( $v->{streaming} && $v->{retry} > 0 ) {
$! = EINTR;
return undef;
Expand Down Expand Up @@ -757,9 +762,10 @@ sub _getNextTrack {

# What type of stream do we have?
if (!$track->{manifest_url}) {
my $videoId = $class->getId($masterUrl);
my $handler = $config->{'format'} =~ /aac/ ?
Plugins::YouTube::M4a->new($track->{url}) :
Plugins::YouTube::WebM->new($track->{url});
Plugins::YouTube::WebM->new($track->{url}, $videoId);

$config->{'handler'} = $handler;
$config->{'sysread'} = \&sysread_URL;
Expand Down
172 changes: 109 additions & 63 deletions plugin/WebM.pm
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ my $cache = Slim::Utils::Cache->new;

use constant HEADER_CHUNK => 8192;

use constant MAX_EBML => 128*1024;
use constant EBML_NEED => 12;
use constant MAX_EBML => 128*1024;
use constant EBML_NEED => 12;
# give up resync after this many dropped bytes. prebuffer empties
# around here anyway so further searching just delays the skip.
use constant MAX_RESYNC_DROP => 4 * 1024 * 1024;

use constant WEBM_ERROR => 0;
use constant WEBM_DONE => 1;
Expand Down Expand Up @@ -76,19 +79,19 @@ my $log = logger('plugin.youtube');
}

sub new {
my ($class, $url) = @_;
my ($class, $url, $videoId) = @_;
my $self = $class->SUPER::new;

# _context is to be flushed/initialized each time the getAudio is restarted
# but _webm is to be used for the duration of the objects, i.e. when seeking
# but _webm is to be used for the duration of the objects, i.e. when seeking
$self->init_accessor(
url => $url,
_context => {},
_webm => {},
);
_webm => { videoId => $videoId },
);

return bless $self, $class;
}
}

sub flush {
$_[0]->_context( { } );
Expand All @@ -108,30 +111,31 @@ sub duration {
}

sub getCues {
my ($v) = @_;

my ($v, $videoId) = @_;
my $tag = $videoId ? "[$videoId] " : "";

# just in case caller sends less than EBML_NEED at first call ... stupid but who knows
return WEBM_MORE if ($v->{need} > length $v->{inBuf});

if ( !defined $v->{id} ) {
my $id;
my $size;

getEBML(\$v->{inBuf}, \$id, \$size);

if ($id ne ID_CUES) {
$log->error("wrong cues offset" );
$log->error("${tag}wrong cues offset" );
return WEBM_ERROR;
}
}

$v->{id} = $id;
$v->{need} = $size;

if ($size > MAX_EBML) {
$log->error("EBML too large: $size");
$log->error("${tag}EBML too large in cues: $size");
return WEBM_ERROR;
}
}
}

if ( $v->{need} <= length $v->{inBuf} ) {
$v->{outBuf} = substr( $v->{inBuf}, 0, $v->{need} );
Expand All @@ -144,34 +148,35 @@ sub getCues {
sub getHeaders {
my ($self, $v) = @_;
my $webm = $self->_webm;

my $tag = $webm->{videoId} ? "[$webm->{videoId}] " : "";

# process all we can
while ($v->{need} <= length $v->{inBuf}) {
my $id;
my $size;
# first need to acquired an ID

# first need to acquired an ID
if ( !defined $v->{id} ) {
$v->{position} += getEBML(\$v->{inBuf}, \$id, \$size);

if ($id eq ID_CLUSTER) {
$log->error("no info found");
$log->error("${tag}no info found");
return WEBM_ERROR;
} elsif ($id eq ID_SEGMENT) {
$webm->{segment_offset} = $v->{position};
next;
} elsif ($id eq ID_TRACKS) {
next;
}

$v->{id} = $id;
$v->{need} = $size;

if ($size > MAX_EBML) {
$log->error("EBML too large: $size");
$log->error("${tag}EBML too large in headers: $size");
return WEBM_ERROR;
}

next;
}

Expand Down Expand Up @@ -212,7 +217,8 @@ sub getAudio {
my ($self, $outBuf) = @_;
my $v = $self->_context;
my $webm = $self->_webm;

my $tag = $webm->{videoId} ? "[$webm->{videoId}] " : "";

$v->{need} //= EBML_NEED; # number of bytes in buffr to allow processing
$v->{position} //= 0; # number of bytes processed from buffer since 1st call

Expand All @@ -228,22 +234,52 @@ sub getAudio {
my $id;
my $size;

# first need to acquired an ID
# first need to acquired an ID
if ( !defined $v->{id} ) {
$v->{position} += getEBML(\$v->{inBuf}, \$id, \$size);

# skip id that are not CLUSTER *except* the SEGMENT !
main::DEBUGLOG && $log->is_debug && $log->debug("cluster of $size byte (last audio $v->{timecode} ms)") if ($id eq ID_CLUSTER);
next if $id eq ID_CLUSTER || $id eq ID_SEGMENT;
$v->{need} = $size;

$v->{need} = $size;
$v->{id} = $id;


# parser desync. YT sometimes drops garbage in the stream.
# look for next CLUSTER id and continue from there. better
# than killing the whole song.
if ($size > MAX_EBML) {
$log->error("EBML too large: $size");
return WEBM_ERROR;
$v->{resyncDropped} //= 0;
$log->warn("${tag}EBML too large: $size, attempting resync (dropped so far: $v->{resyncDropped})");
my $sync = index($v->{inBuf}, ID_CLUSTER);
if ($sync >= 0) {
substr($v->{inBuf}, 0, $sync, '');
$v->{position} += $sync;
$v->{resyncDropped} += $sync;
$v->{need} = EBML_NEED;
undef $v->{id};
main::INFOLOG && $log->is_info && $log->info("${tag}resync ok, dropped $v->{resyncDropped} bytes total");
$v->{resyncDropped} = 0;
next;
}
# no cluster in buffer yet. drop most of it, ask for more.
# but if we have already dropped a lot give up so the
# player can move on instead of stalling silently.
my $drop = length($v->{inBuf}) - length(ID_CLUSTER) + 1;
if ($drop > 0) {
substr($v->{inBuf}, 0, $drop, '');
$v->{position} += $drop;
$v->{resyncDropped} += $drop;
}
if ($v->{resyncDropped} > MAX_RESYNC_DROP) {
$log->error("${tag}resync failed after $v->{resyncDropped} bytes, giving up");
return WEBM_ERROR;
}
$v->{need} = EBML_NEED;
undef $v->{id};
return WEBM_MORE;
}

next;
}

Expand Down Expand Up @@ -539,11 +575,17 @@ sub getEBML {
for ($len = 1; !($c & 0x80) && $len <= 4; $len++) { $c <<= 1 }

if ($len == 5) {
$log->error("wrong len: $len, $c");
# arbitrary, but at least won't get stuck in the an infinite loop and will not go beyond available data
$len = EBML_NEED;
substr($$in, 0, $len, '');
return $len;
# desync. element_id length walk fell off the end, the first byte
# does not look like an EBML id. drop just one byte and let the
# caller try again. earlier we used to drop EBML_NEED (12) which
# threw away around 3s of audio every desync. now we lose one
# byte and the cluster resync in getAudio normally picks up
# within a packet or two.
main::INFOLOG && $log->is_info && $log->info("getEBML desync, dropping 1 byte");
substr($$in, 0, 1, '');
$$id = undef;
$$size = 0;
return 1;
}

$$id = substr($$in, 0, $len, '');
Expand Down Expand Up @@ -660,53 +702,57 @@ sub getStartOffset {
onStream => sub {
my ($http, $dataref) = @_;

$var->{'inBuf'} .= $$dataref;
my $res = getCues($var);
$var->{'inBuf'} .= $$dataref;
my $res = getCues($var, $webm->{videoId});

if ( $res eq WEBM_MORE ) {
return 1 if $$dataref ne '';
$cb->( $webm->{offset}->{clusters} );
return 0;
} elsif ( $res eq WEBM_DONE ) {
return 0;
} elsif ( $res eq WEBM_DONE ) {
my $offset = getCueOffset($var->{outBuf}, $startTime*($webm->{timecode_scale}/1000000)*1000) + $webm->{segment_offset};
$cb->($offset);
return 0;
} elsif ( $res eq WEBM_ERROR ) {
$log->warn( "could not find start offset" );
my $tag = $webm->{videoId} ? "[$webm->{videoId}] " : "";
$log->warn( "${tag}could not find start offset" );
$cb->( $webm->{offset}->{clusters} );
return 0;
}
},
onError => sub {
my ($self, $error) = @_;
$log->warn( "could not find start offset $error" );
my $tag = $webm->{videoId} ? "[$webm->{videoId}] " : "";
$log->warn( "${tag}could not find start offset $error" );
$cb->( $webm->{offset}->{clusters} );
},
} );
}

sub initialize {
my ($self, $cb, $ecb) = @_;
my $webm = $self->_webm;
my $tag = $webm->{videoId} ? "[$webm->{videoId}] " : "";
my $var = { 'inBuf' => '',
'id' => undef,
'need' => EBML_NEED,
'id' => undef,
'need' => EBML_NEED,
};

my $http = Slim::Networking::Async::HTTP->new;

$http->send_request( {
request => HTTP::Request->new( GET => $self->url ),
onStream => sub {
onStream => sub {
my ($http, $dataref) = @_;

$var->{'inBuf'} .= $$dataref;
my $res = $self->getHeaders($var);

if ( $res eq WEBM_MORE ) {
return 1;
} elsif ( $res eq WEBM_DONE ) {
} elsif ( $res eq WEBM_DONE ) {
my $info = $self->_webm->{track};

$self->channels($info->{channels});
$self->samplerate($info->{samplerate});
$self->bitrate($info->{bitrate});
Expand All @@ -718,17 +764,17 @@ sub initialize {
$cb->();
return 0;
} elsif ( $res eq WEBM_ERROR ) {
$log->error( "could not get webm headers" );
$log->error( "${tag}could not get webm headers" );
$ecb->();
return 0;
}
}
},
onError => sub {
my ($self, $error) = @_;
$log->warn("could not get codec info $error");
$log->warn("${tag}could not get codec info $error");
$ecb->();
}
} );
} );
}

1;