diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Control/IPSpeaker.pm b/scripts/ZoneMinder/lib/ZoneMinder/Control/IPSpeaker.pm index e4c173ff5..069db7ebb 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Control/IPSpeaker.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Control/IPSpeaker.pm @@ -57,6 +57,10 @@ use constant VOLUME_MIN => 0; use constant VOLUME_MAX => 100; use constant VOLUME_STEP => 5; +# Seconds. The speaker answers in milliseconds when it is there at all, so this +# only ever bounds the case where it is not. +use constant HTTP_TIMEOUT => 10; + # config=audio.set replaces the whole audio section: any field left out of the # POST reverts to a firmware default, silently taking the microphone, codec # list and echo-cancellation settings with it. Every write therefore reads the @@ -121,7 +125,11 @@ sub open { my $self = shift; $self->loadMonitor(); - $self->{ua} = LWP::UserAgent->new; + # Actions are fire-and-forget from the monitor's point of view, but they are + # serialised through this one control daemon, so a speaker that has dropped + # off the network must not hold it. LWP's default is 180s, long enough for a + # single unreachable speaker to stall every later command behind it. + $self->{ua} = LWP::UserAgent->new(timeout => HTTP_TIMEOUT); $self->{ua}->agent('ZoneMinder Control Agent/'.ZoneMinder::Base::ZM_VERSION()); if (!$self->guess_credentials()) { diff --git a/scripts/ZoneMinder/t/ip_speaker.t b/scripts/ZoneMinder/t/ip_speaker.t index 47aabcc83..16115b07a 100644 --- a/scripts/ZoneMinder/t/ip_speaker.t +++ b/scripts/ZoneMinder/t/ip_speaker.t @@ -1,6 +1,6 @@ use strict; use warnings; -use Test::More tests => 32; +use Test::More tests => 35; require_ok('ZoneMinder::Control::IPSpeaker'); @@ -77,3 +77,14 @@ my $sparse_form = $P->can('audio_set_form')->(\%sparse, outvolume => 0); is_deeply([sort keys %$sparse_form], [qw(micvolume outvolume)], 'fields absent from the device response are not invented'); is($sparse_form->{outvolume}, 0, 'a zero override is applied, not treated as absent'); + +# --- the http timeout ------------------------------------------------------- +# A speaker that drops off the network must not stall the control daemon. LWP +# defaults to 180s, and every action for this monitor queues behind the one in +# flight, so an absent speaker would hold up everything that followed it. + +is(ZoneMinder::Control::IPSpeaker::HTTP_TIMEOUT(), 10, 'the http timeout is set, not left to LWP'); +cmp_ok(ZoneMinder::Control::IPSpeaker::HTTP_TIMEOUT(), '<', 180, + 'and is well under the LWP default that caused the stall'); +cmp_ok(ZoneMinder::Control::IPSpeaker::HTTP_TIMEOUT(), '>', 0, + 'and is a real timeout rather than "no wait"');