Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create "Buffered" player protocol #581

Merged
merged 6 commits into from
Apr 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions Changelog8.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ <h2><a name="v8.2.0" id="v8.2.0"></a>Version 8.2.0</h2>
<li><a href="https://github.com/Logitech/slimserver/pull/537">#537</a> - Add audio option to combine channels to build a mono signal (whether player is synchronized or not).</li>
<li><a href="https://github.com/Logitech/slimserver/pull/538">#538</a> - Add Balance setting for players which support it (thanks philippe44!).</li>
<li>Enable basic track statistics (play count, last played, ratings) for online tracks imported into the library.</li>
<li><a href="https://github.com/Logitech/slimserver/pull/581">#581</a> - Create new player protocol to buffer http streams to disk to improve reliability (thanks philippe44!).</li>
</ul>
<br />

Expand Down
9 changes: 9 additions & 0 deletions HTML/EN/settings/server/performance.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@
</select>
[% END %]

[% WRAPPER setting title="SETUP_BUFFEREDHTTP" desc="SETUP_BUFFEREDHTTP_DESC" %]
<select class="stdedit" name="pref_useBufferedHTTP" id="useBufferedHTTP">

<option [% IF NOT prefs.pref_useBufferedHTTP %]selected [% END %]value="0">[% 'SETUP_DISABLE_BUFFEREDHTTP' | getstring %]</option>
<option [% IF prefs.pref_useBufferedHTTP %]selected [% END %]value="1">[% 'SETUP_ENABLE_BUFFEREDHTTP' | getstring %]</option>

</select>
[% END %]

[% IF prioritySettings; WRAPPER settingSection %]
[% WRAPPER settingGroup title="SETUP_SERVERPRIORITY" desc="SETUP_SERVERPRIORITY_DESC" %]
<select class="stdedit" name="pref_serverPriority" id="serverPriority">
Expand Down
40 changes: 26 additions & 14 deletions Slim/Player/ProtocolHandlers.pm
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,28 @@ use Tie::RegexpHash;

use Slim::Utils::Log;
use Slim::Utils::Misc;
use Slim::Utils::Prefs;
use Slim::Music::Info;
use Slim::Networking::Async::HTTP;

my $prefs = preferences('server');

# the protocolHandlers hash contains the modules that handle specific URLs,
# indexed by the URL protocol. built-in protocols are exist in the hash, but
# have a zero value
my %protocolHandlers = (
my %protocolHandlers = (
file => main::LOCALFILE ? qw(Slim::Player::Protocols::LocalFile) : qw(Slim::Player::Protocols::File),
tmp => qw(Slim::Player::Protocols::Volatile),
http => qw(Slim::Player::Protocols::HTTP),
https => Slim::Networking::Async::HTTP->hasSSL() ? qw(Slim::Player::Protocols::HTTPS) : qw(Slim::Player::Protocols::HTTP),
icy => qw(Slim::Player::Protocols::HTTP),
mms => qw(Slim::Player::Protocols::MMS),
spdr => qw(Slim::Player::Protocols::SqueezePlayDirect),
playlist => 0,
db => 1,
);

setHTTPHandler($prefs->get('useBufferedHTTP'));

$prefs->setChange(sub { setHTTPHandler($_[1]) }, 'useBufferedHTTP');

tie my %URLHandlers, 'Tie::RegexpHash';

my %localHandlers = (
Expand All @@ -42,14 +46,22 @@ my %loadedHandlers = ();

my %iconHandlers = ();

sub setHTTPHandler {
my ($useBufferedHTTP) = @_;

$protocolHandlers{http} = $useBufferedHTTP ? qw(Slim::Player::Protocols::Buffered) : qw(Slim::Player::Protocols::HTTP),
$protocolHandlers{https} = $useBufferedHTTP ? qw(Slim::Player::Protocols::Buffered) : (Slim::Networking::Async::HTTP->hasSSL() ? qw(Slim::Player::Protocols::HTTPS) : qw(Slim::Player::Protocols::HTTP)),
$protocolHandlers{icy} = $useBufferedHTTP ? qw(Slim::Player::Protocols::Buffered) : qw(Slim::Player::Protocols::HTTP),
}

sub isValidHandler {
my ($class, $protocol) = @_;

if (defined $protocol) {
if ($protocolHandlers{$protocol}) {
return 1;
}

if (exists $protocolHandlers{$protocol}) {
return 0;
}
Expand All @@ -60,9 +72,9 @@ sub isValidHandler {

sub isValidRemoteHandler {
my ($class, $protocol) = @_;

return isValidHandler(@_) && !$localHandlers{$protocol};

}

sub registeredHandlers {
Expand All @@ -73,7 +85,7 @@ sub registeredHandlers {

sub registerHandler {
my ($class, $protocol, $classToRegister) = @_;

$protocolHandlers{$protocol} = $classToRegister;
}

Expand All @@ -92,7 +104,7 @@ sub registerIconHandler {

sub handlerForProtocol {
my ($class, $protocol) = @_;

return $protocolHandlers{$protocol};
}

Expand Down Expand Up @@ -121,7 +133,7 @@ sub iconHandlerForURL {
my ($class, $url) = @_;

return undef unless $url;

my $handler;
foreach (keys %iconHandlers) {
if ($url =~ /$_/i) {
Expand All @@ -136,7 +148,7 @@ sub iconHandlerForURL {

sub iconForURL {
my ($class, $url, $client) = @_;

$url ||= '';

if (my $handler = $class->handlerForURL($url)) {
Expand All @@ -145,7 +157,7 @@ sub iconForURL {
return $meta->{cover} if $meta->{cover};
}
}

if ($handler->can('getIcon')) {
return $handler->getIcon($url);
}
Expand All @@ -157,12 +169,12 @@ sub iconForURL {

elsif ($url =~ /^db:album\.(\w+)=(.+)/) {
my $value = Slim::Utils::Misc::unescape($2);

if (utf8::is_utf8($value)) {
utf8::decode($value);
utf8::encode($value);
}

my $album = Slim::Schema->search('Album', { $1 => $value })->first;

if ($album && $album->artwork) {
Expand Down
99 changes: 99 additions & 0 deletions Slim/Player/Protocols/Buffered.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package Slim::Player::Protocols::Buffered;

use strict;
use base qw(Slim::Player::Protocols::HTTPS);

use File::Temp;

use Slim::Utils::Errno;
use Slim::Utils::Log;
use Slim::Utils::Prefs;

my $log = logger('player.streaming.remote');
my $prefs = preferences('server');

sub canDirectStream { 0 }

sub new {
my $class = shift;
my ($args) = @_;

my $self = $class->SUPER::new(@_) or do {
$log->error("Couldn't create socket for Buffered - $!");
return undef;
};

# don't buffer if we don't have content-length
return $self unless ${*$self}{'contentLength'};

main::INFOLOG && $log->info("Using Buffered HTTP(S) service for $args->{url}");

# HTTP headers have now been acquired in a blocking way by the above, we can
# now enable fast download of body to a file from which we'll read further data
# but the switch of socket handler can only be done within _sysread otherwise
# we will timeout when there is a pipeline with a callback
${*$self}{'_fh'} = File::Temp->new( DIR => Slim::Utils::Misc::getTempDir, SUFFIX => '.buf' );
open ${*$self}{'_rfh'}, '<', ${*$self}{'_fh'}->filename;
binmode(${*$self}{'_rfh'});

return $self;
}

sub close {
my $self = shift;

# clean buffer file and all handlers
Slim::Networking::Select::removeRead($self);
${*$self}{'_rfh'}->close if ${*$self}{'_rfh'};
delete ${*$self}{'_fh'};

$self->SUPER::close(@_);
}

# we need that call structure to make sure that SUPER calls the
# object's parent, not the package's parent
# see http://modernperlbooks.com/mt/2009/09/when-super-isnt.html
sub _sysread {
my $self = $_[0];
my $rfh = ${*$self}{'_rfh'};

# we are not ready to read body yet, read socket directly
return $self->SUPER::_sysread($_[1], $_[2], $_[3]) unless $rfh;

# first, try to read from buffer file
my $readLength = $rfh->read($_[1], $_[2], $_[3]);
return $readLength if $readLength;

# assume that close() will be called for cleanup
return 0 if ${*$self}{_done};

# empty file but not done yet, try to read directly
$readLength = $self->SUPER::_sysread($_[1], $_[2], $_[3]);

# if we now have data pending, likely we have been removed from the reading loop
# so we have to re-insert ourselves (no need to store fresh data in buffer)
if ($readLength) {
Slim::Networking::Select::addRead($self, \&saveStream);
return $readLength;
}

# use EINTR because EWOULDBLOCK (although faster) may overwrite our addRead()
$! = EINTR;
return undef;
}

sub saveStream {
my $self = shift;

my $bytes = $self->SUPER::_sysread(my $data, 32768);
return unless defined $bytes;

if ($bytes) {
# need to bypass Perl's buffered IO and maje sure read eof is reset
syswrite(${*$self}{'_fh'}, $data);
${*$self}{'_rfh'}->seek(0, 1);
} else {
Slim::Networking::Select::removeRead($self);
${*$self}{_done} = 1;
}
}
15 changes: 12 additions & 3 deletions Slim/Plugin/WiMP/ProtocolHandler.pm
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ package Slim::Plugin::WiMP::ProtocolHandler;
# version 2.

use strict;
use base qw(Slim::Player::Protocols::HTTPS);
use vars qw(@ISA);

use JSON::XS::VersionOneAndTwo;
use URI::Escape qw(uri_escape_utf8);
Expand All @@ -25,6 +25,15 @@ my $log = Slim::Utils::Log->addLogCategory( {
'description' => 'PLUGIN_WIMP_MODULE_NAME',
} );

if ($prefs->get('useBufferedHTTP')) {
require Slim::Player::Protocols::Buffered;
push @ISA, qw(Slim::Player::Protocols::Buffered);
}
else {
require Slim::Player::Protocols::HTTPS;
push @ISA, qw(Slim::Player::Protocols::HTTPS);
}

# https://tidal.com/browse/track/95570766
# https://tidal.com/browse/album/95570764
# https://tidal.com/browse/playlist/5a36919b-251c-4fa7-802c-b659aef04216
Expand Down Expand Up @@ -283,8 +292,8 @@ sub _gotTrack {
my $meta = $cache->get('wimp_meta_' . $info->{id});
$meta->{bitrate} = sprintf("%.0f" . Slim::Utils::Strings::string('KBPS'), $song->track->bitrate/1000);
$cache->set( 'wimp_meta_' . $info->{id}, $meta, 86400 );
$params->{successCb}->();
} },
$params->{successCb}->();
} },
$info->{url} ],
} );
} else {
Expand Down
2 changes: 1 addition & 1 deletion Slim/Web/Settings/Server/Performance.pm
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ sub page {
}

sub prefs {
my @prefs = ( $prefs, qw(dbhighmem disableStatistics serverPriority scannerPriority
my @prefs = ( $prefs, qw(dbhighmem disableStatistics serverPriority scannerPriority useBufferedHTTP
precacheArtwork maxPlaylistLength useLocalImageproxy dontTriggerScanOnPrefChange) );
push @prefs, qw(autorescan autorescan_stat_interval) if Slim::Utils::OSDetect::getOS->canAutoRescan;
return @prefs;
Expand Down
16 changes: 16 additions & 0 deletions strings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8414,6 +8414,22 @@ SETUP_ENABLE_STATISTICS
SV Aktivera biblioteksstatistik
ZH_CN 启用库存统计

SETUP_BUFFEREDHTTP
DE Online Datenströme zwischenspeichern
EN Cache online streams to disk

SETUP_BUFFEREDHTTP_DESC
DE Logitech Media Server kann HTTP(S) Datenströme mit definierter Länge (Podcasts, einzelne Lieder) im Dateisystem zwischenspeichern. Sie würden wieder gelöscht, sobald der Strom zu Ende gespielt ist. Dies erhöht die Zuverlässigkeit mit einigen Anbietern, kann aber das Dateisystem zusätzlich belasten (z.B. wenn eine SD Karte verwendet wird).
EN Logitech Media Server can cache HTTP(S) streams of defined length (podcasts, single tracks) on disk. The buffer files will be removed again once the stream has been played. This can improve the reliability with some servers. But it also does cause more writing to the file system, which could potentially wear out eg. SD cards.

SETUP_ENABLE_BUFFEREDHTTP
DE HTTP(S) Datenströme zwischenspeichern
EN Cache HTTP(S) streams on disk

SETUP_DISABLE_BUFFEREDHTTP
DE HTTP(S) Datenströme nicht zwischenspeichern
EN Do not cache HTTP(S) streams on disk

SETUP_LONGDATEFORMAT
CS Formát dlouhého data
DA Langt datoformat
Expand Down