#!/bin/sh -e
# Plays and records media clips (currently only audio clips).
#
# Example usage:
#
# Play clip specified by MediaClip.M2:
# mediaclip.cgi?action=play&clip=2
#
# Wait 5 seconds and then record new clip during 10 seconds:
# mediaclip.cgi?action=record&wait=5&duration=10
#
# Parameter 'generate_header' may be used to skip response header.
. /usr/html/axis-cgi/lib/functions.sh
readonly AUDIO_FILE_PATH=/etc/audioclips
readonly PHC="parhandclient --nocgi"
readonly SAMPLE_RATE_DEFAULT=16000
readonly LOGW="logger -pwarning -t${0##*/} -s"
# CGI compliant by default
CGI_HDGEN=$(__qs_getparam generate_header) || CGI_HDGEN=yes
printout() {
if [ "$CGI_HDGEN" = no ]; then
printf "%s\n" "$2"
[ $# -lt 3 ] || printf "%s\n" "$3"
else
__cgi_errhd $1 "$2"
[ $# -lt 3 ] || printf "%s\r\n" "$3"
CGI_HDGEN=no # To prevent duplicated header.
fi
}
ok() {
printout 200 "OK" "$1=$2"
}
# Send error reply to post requests
# Must read file upload before sending
error_post() {
cat >/dev/null
printout 400 "$*"
exit 1
}
error() {
case "$1" in
4??)
printout $1 "$2"
;
*)
printout 400 "$*"
;
esac
exit 1
}
# @$1: set|cancel LED state
# @$2: LED state
dbus_set_led() {
local cmd
case $1 in
set)
cmd=SetState
;
cancel)
cmd=CancelState
;
*)
error "Bad command '$1' for LED state '$2'"
;
esac
gdbus call -y -d com.axis.LEDController -o /com/axis/LEDController \
-m com.axis.LEDController.$cmd "$2" >/dev/null 2>&1 || :
}
# @$1: play|record
# @$2: options for @$1 command
dbus_send_audio() {
local method
case $1 in
play)
method=StartPlayingClip
;
rec)
method=RecordClip
;
*)
;
esac
gdbus call -y --dest=com.axis.Streamer -o /com/axis/Streamer/Audio \
-m com.axis.Streamer.Audio.$method "$2" 2>&1
}
# Remove file:// prefix if there
# if not http:// check so it starts with $AUDIO_FILE_PATH/
# and not contain ..
sane_location() {
LOCATION=${LOCATION#file://}
if [ "${LOCATION##http://}" = "$LOCATION" ]; then
[ "${LOCATION##$AUDIO_FILE_PATH/}" != "$LOCATION" ] || return 1
case $LOCATION in
*/../*|../*|*/..|..)
return 2
;
esac
fi
}
# @$1: action
check_num_clips() {
local max_mediaclips nbr_of_groups errf=error
[ "$1" != upload ] || errf=error_post
max_mediaclips=$($PHC get MediaClip.MaxGroups - RAW) ||
error "Maximum number of allowed audioclips not found in the '$1' action."
nbr_of_groups=$($PHC getgrouplist MediaClip | wc -l)
[ $nbr_of_groups -ne $max_mediaclips ] ||
$errf "Failed to $1 more audioclips." \
"The maximum number of permitted audio clips" \
"'$max_mediaclips' has been reached."
}
# @$1: action
# @$2: value id
# @$3: value to test
is_integer() {
case $3 in
*[!0-9]*)
error "Parameter '$2' must be an integer in the '$1' action."
;
esac
}
# Arg 1: group (A0, A1...)
has_audio_output() {
local output
[ $# -eq 1 ] && [ "$1" ] || return 1
output=$($PHC get Properties.Audio.Source.$1.Output - RAW) ||
error "Unable to fetch audio output $1"
[ "$output" = yes ]
}
# $1 - variable name to return the source with
get_default_output_source() {
local f=get_default_output_source sources source
[ $# -eq 1 ] && [ "$1" ] || error "$f: missing argument"
# Get available audio sources
sources=$($PHC getgrouplist AudioSource - RAW) ||
error "Unable to fetch audio sources"
for source in $sources; do
if has_audio_output $source; then
eval $1=\$source
return 0
fi
done
error "No default audio output found"
}
play() {
local clip_nr ret group_nbr default_source audiooutput
clip_nr=$(__qs_getparam clip) && [ "$clip_nr" ] ||
error "Missing parameter: clip required in the 'play' action."
is_integer play clip_nr "$clip_nr"
LOCATION=$($PHC get MediaClip.M$clip_nr.Location - RAW |
urldecode -e) ||
error "Clip number '$clip_nr' not found in the 'play' action."
audiooutput=$(__qs_getparam audiooutput) || audiooutput=
if [ "$audiooutput" ]; then
is_integer play audiooutput "$audiooutput"
# Confirm that provided audio source exists and has output
group_nbr=$(($audiooutput - 1))
has_audio_output A$group_nbr ||
error "No output for AudioSource.A$group_nbr"
else
# Get default AudioSource number with audio output, then increase by 1
get_default_output_source default_source
group_nbr=${default_source#?}
audiooutput=$(($group_nbr + 1))
fi
ret=$(dbus_send_audio play "location=$LOCATION&audiooutput=$audiooutput") ||
error "Failed to play clip '$clip_nr': '$ret'."
ok "playing" $clip_nr
}
record() {
local samplerate mediatype duration wait clip_nr ret name statusled cnt
local channel
samplerate=$(__qs_getparam samplerate) ||
samplerate=$SAMPLE_RATE_DEFAULT
case $samplerate in
$SAMPLE_RATE_DEFAULT|8000)
;
*)
error "Samplerate '$samplerate' not supported in the 'record' action."
;
esac
mediatype=$(__qs_getparam media) && [ "$mediatype" ] ||
error "Missing parameter: Media type required in the 'record' action."
[ "$mediatype" = audio ] ||
error "Unknown media type in the 'record' action." \
"Supported media types: audio"
duration=$(__qs_getparam duration) || duration=10
is_integer record duration "$duration"
wait=$(__qs_getparam wait) || wait=0
is_integer record wait "$wait"
check_num_clips record
clip_nr=$($PHC addgroup MediaClip mediaclip) ||
error "Failed to add clip to parameters: '$clip_nr' in the 'record' action."
LOCATION=$AUDIO_FILE_PATH/$clip_nr.au
ret=$($PHC set $clip_nr.Location $LOCATION) ||
error "Failed to set clip location: '$ret' in the 'record' action."
name=$(__qs_getparam name) && [ "$name" ] ||
name="Clip $clip_nr"
ret=$($PHC set $clip_nr.Name "$name") ||
error "Failed to set clip name: '$ret' in the 'record' action."
statusled=$(__qs_getparam statusled) && [ "$statusled" ] ||
statusled=yes
channel=$(__qs_getparam audiochannel) || channel=1
[ "$statusled" = no ] || dbus_set_led set LED_STATE_AUDIOCLIP_WAIT
[ $wait -le 0 ] || sleep $wait
ret=$(dbus_send_audio rec "location=$LOCATION&duration=$duration&audio=1&audiocodec=g711&audiosamplerate=$samplerate&audiochannel=$channel") ||
error "Failed to record clip '$clip_nr': '$ret'."
ok "recording" $clip_nr
[ "$statusled" = no ] || dbus_set_led set LED_STATE_AUDIOCLIP_RECORD
[ $duration -le 0 ] || {
sleep $duration
cnt=5
while [ ! -f $LOCATION ] && [ $cnt -gt 0 ]; do
cnt=$(($cnt - 1))
sleep 1
done
}
[ "$statusled" = no ] || {
dbus_set_led cancel LED_STATE_AUDIOCLIP_RECORD
dbus_set_led cancel LED_STATE_AUDIOCLIP_WAIT
}
}
update() {
local clip_nr clip_name
clip_nr=$(__qs_getparam clip) ||
error "Missing parameter: clip required in the 'update' action."
is_integer update clip_nr "$clip_nr"
clip_name=$(__qs_getparam name) ||
error "Missing parameter: name required in the 'update' action."
[ "$clip_name" ] || error "Clip name must not be blank."
$PHC set MediaClip.M$clip_nr.Name "$clip_name" ||
error "Failed to update clip '$clip_nr'."
ok "updated" $clip_nr
}
remove() {
local clip_nr
clip_nr=$(__qs_getparam clip) && [ "$clip_nr" ] ||
error "Missing parameter: clip required in the 'remove' action."
is_integer remove clip_nr "$clip_nr"
LOCATION=$($PHC get MediaClip.M$clip_nr.Location - RAW) ||
error "Clip number '$clip_nr' not found in the 'remove' action."
$PHC deletegroup MediaClip.M$clip_nr ||
error "Failed to remove clip parameter 'M$clip_nr'."
sane_location ||
$LOGW "Clip location not valid, ignoring removal:" \
"'$LOCATION'"
[ "${LOCATION##http://}" != "$LOCATION" ] ||
rm -f "$LOCATION" ||
$LOGW "Failed to remove clip location '$LOCATION': $?."
ok "removed" $clip_nr
}
download() {
local clip_nr mimetype
clip_nr=$(__qs_getparam clip) && [ "$clip_nr" ] ||
error "Missing parameter: clip required in the 'download' action."
is_integer download clip_nr "$clip_nr"
LOCATION=$($PHC get MediaClip.M$clip_nr.Location - RAW) ||
error "Clip number '$clip_nr' not found in the 'download' action."
sane_location || error "Clip location not valid in the 'download' action."
[ -r "$LOCATION" ] ||
error "Clip location '$LOCATION' not found in the 'download' action."
if [ ${LOCATION##*.} = au ]; then
mimetype=audio/basic
else
mimetype=audio/x-wav
fi
__cgi_hdgen yes "$(printf '%s\r\nContent-Disposition: attachment; filename="%s"' $mimetype "${LOCATION##*/}")"
cat "$LOCATION" || exit 1
}
# @$1: file location
# @$2: error message
# @$3: error code
on_failed_file_operation() {
[ $# -ge 1 ] && [ "$1" ] || error "Invalid file argument"
[ ! -f "$1" ] || rm "$1" || error "Unable to remove file $1"
if [ $# -gt 2 ]; then
error $3 "$2"
elif [ $# -eq 2 ]; then
error "$2"
else
error "File operation failed"
fi
}
upload() {
local mediatype clip_nr ret name existing_param
mediatype=$(__qs_getparam media) && [ "$mediatype" ] ||
error_post "Missing parameter: media required in the 'upload' action."
[ "$mediatype" = audio ] ||
error_post "Unknown media type in the 'upload' action." \
"Supported media types: audio"
check_num_clips upload
LOCATION=$(file_upload -s $CONTENT_LENGTH -n 65536 -a \
--allowed-realms= --allowed-dirs=audioclips --default-dir=audioclips) ||
error 413 "Failed to upload clip. ($?) size=$CONTENT_LENGTH"
chgrp streamer "$LOCATION" && chmod 640 "$LOCATION" ||
on_failed_file_operation $LOCATION "Failed to change owner and/or " \
"permissions on '$LOCATION' in the 'upload' action."
audio_file is_supported_audio "$LOCATION" ||
on_failed_file_operation $LOCATION "Unsupported Media Type" 415
# Retrieve, if any: mediaclip.m[$clip_nr].Location=[$LOCATION]
existing_param=$($PHC --nolog getgroup MediaClip 2>/dev/null | grep "$LOCATION") || :
# If $LOCATION already exist in any MediaClip group
if [ "$existing_param" ]; then
# Extract and return existing parameter group number
clip_nr=${existing_param#root.}
clip_nr=${clip_nr%.Location=*}
ok "updated" $clip_nr
else
clip_nr=$($PHC addgroup MediaClip mediaclip) ||
on_failed_file_operation $LOCATION \
"Failed to add clip to parameters: '$clip_nr' in the 'upload' action."
ret=$($PHC set $clip_nr.Location "$LOCATION") ||
on_failed_file_operation $LOCATION \
"Failed to set clip location: '$ret' in the 'upload' action."
name=$(__qs_getparam name) && [ "$name" ] ||
name="Clip $clip_nr"
ret=$($PHC set $clip_nr.Name "$name") ||
on_failed_file_operation $LOCATION "Failed to set clip name: '$ret' " \
"in the 'upload' action."
ok "uploaded" $clip_nr
fi
}
action=$(__qs_getparam action) && [ "$action" ] ||
error "Missing parameter: action argument is required."
case $action in
play|record|update|remove|download|upload)
$action
;
*)
error "Unknown action: '$action'." \
"action argument is required."
;
esac