openvidu-server, openvidu-node-client, openvidu-java-client: Add COMPOSED_QUICK_START outputMode

pull/508/head
cruizba 2020-07-01 14:02:32 +02:00
parent 45d3ba6078
commit 0a02ae8059
22 changed files with 1036 additions and 845 deletions

View File

@ -201,7 +201,8 @@ public class OpenVidu {
json.addProperty("hasAudio", properties.hasAudio()); json.addProperty("hasAudio", properties.hasAudio());
json.addProperty("hasVideo", properties.hasVideo()); json.addProperty("hasVideo", properties.hasVideo());
if (Recording.OutputMode.COMPOSED.equals(properties.outputMode()) && properties.hasVideo()) { if ((Recording.OutputMode.COMPOSED.equals(properties.outputMode()) || (Recording.OutputMode.COMPOSED_QUICK_START.equals(properties.outputMode())))
&& properties.hasVideo()) {
json.addProperty("resolution", properties.resolution()); json.addProperty("resolution", properties.resolution());
json.addProperty("recordingLayout", json.addProperty("recordingLayout",
(properties.recordingLayout() != null) ? properties.recordingLayout().name() : ""); (properties.recordingLayout() != null) ? properties.recordingLayout().name() : "");

View File

@ -74,7 +74,14 @@ public class Recording {
/** /**
* Record each stream individually * Record each stream individually
*/ */
INDIVIDUAL; INDIVIDUAL,
/**
* EXPERIMENTAL
* This option is intended to keep a recorder session openned for
* incoming recording requests to be recorded as fast as possible
*/
COMPOSED_QUICK_START;
} }
private Recording.Status status; private Recording.Status status;
@ -105,7 +112,7 @@ public class Recording {
OutputMode outputMode = OutputMode.valueOf(json.get("outputMode").getAsString()); OutputMode outputMode = OutputMode.valueOf(json.get("outputMode").getAsString());
RecordingProperties.Builder builder = new RecordingProperties.Builder().name(json.get("name").getAsString()) RecordingProperties.Builder builder = new RecordingProperties.Builder().name(json.get("name").getAsString())
.outputMode(outputMode).hasAudio(hasAudio).hasVideo(hasVideo); .outputMode(outputMode).hasAudio(hasAudio).hasVideo(hasVideo);
if (OutputMode.COMPOSED.equals(outputMode) && hasVideo) { if ((OutputMode.COMPOSED.equals(outputMode) || OutputMode.COMPOSED_QUICK_START.equals(outputMode)) && hasVideo) {
builder.resolution(json.get("resolution").getAsString()); builder.resolution(json.get("resolution").getAsString());
builder.recordingLayout(RecordingLayout.valueOf(json.get("recordingLayout").getAsString())); builder.recordingLayout(RecordingLayout.valueOf(json.get("recordingLayout").getAsString()));
JsonElement customLayout = json.get("customLayout"); JsonElement customLayout = json.get("customLayout");

View File

@ -52,7 +52,7 @@ public class RecordingProperties {
* Builder for {@link io.openvidu.java.client.RecordingProperties} * Builder for {@link io.openvidu.java.client.RecordingProperties}
*/ */
public RecordingProperties build() { public RecordingProperties build() {
if (OutputMode.COMPOSED.equals(this.outputMode)) { if (OutputMode.COMPOSED.equals(this.outputMode) || OutputMode.COMPOSED_QUICK_START.equals(this.outputMode)) {
this.recordingLayout = this.recordingLayout != null ? this.recordingLayout : RecordingLayout.BEST_FIT; this.recordingLayout = this.recordingLayout != null ? this.recordingLayout : RecordingLayout.BEST_FIT;
this.resolution = this.resolution != null ? this.resolution : "1920x1080"; this.resolution = this.resolution != null ? this.resolution : "1920x1080";
if (RecordingLayout.CUSTOM.equals(this.recordingLayout)) { if (RecordingLayout.CUSTOM.equals(this.recordingLayout)) {

View File

@ -23,6 +23,7 @@ import { RecordingProperties } from './RecordingProperties';
import { Session } from './Session'; import { Session } from './Session';
import { SessionProperties } from './SessionProperties'; import { SessionProperties } from './SessionProperties';
import { RecordingLayout } from './RecordingLayout'; import { RecordingLayout } from './RecordingLayout';
import { RecordingMode } from 'RecordingMode';
/** /**
* @hidden * @hidden
@ -143,7 +144,8 @@ export class OpenVidu {
hasAudio: !!(properties.hasAudio), hasAudio: !!(properties.hasAudio),
hasVideo: !!(properties.hasVideo) hasVideo: !!(properties.hasVideo)
}; };
if (data.outputMode.toString() === Recording.OutputMode[Recording.OutputMode.COMPOSED]) { if (data.outputMode.toString() === Recording.OutputMode[Recording.OutputMode.COMPOSED]
|| data.outputMode.toString() === Recording.OutputMode[Recording.OutputMode.COMPOSED_QUICK_START]) {
data.resolution = !!properties.resolution ? properties.resolution : '1920x1080'; data.resolution = !!properties.resolution ? properties.resolution : '1920x1080';
data.recordingLayout = !!properties.recordingLayout ? properties.recordingLayout : RecordingLayout.BEST_FIT; data.recordingLayout = !!properties.recordingLayout ? properties.recordingLayout : RecordingLayout.BEST_FIT;
if (data.recordingLayout.toString() === RecordingLayout[RecordingLayout.CUSTOM]) { if (data.recordingLayout.toString() === RecordingLayout[RecordingLayout.CUSTOM]) {

View File

@ -82,7 +82,8 @@ export class Recording {
hasAudio: !!(json['hasAudio']), hasAudio: !!(json['hasAudio']),
hasVideo: !!json['hasVideo'] hasVideo: !!json['hasVideo']
}; };
if (this.properties.outputMode.toString() === Recording.OutputMode[Recording.OutputMode.COMPOSED]) { if (this.properties.outputMode.toString() === Recording.OutputMode[Recording.OutputMode.COMPOSED]
|| this.properties.outputMode.toString() === Recording.OutputMode[Recording.OutputMode.COMPOSED_QUICK_START]) {
this.properties.resolution = !!(json['resolution']) ? json['resolution'] : '1920x1080'; this.properties.resolution = !!(json['resolution']) ? json['resolution'] : '1920x1080';
this.properties.recordingLayout = !!(json['recordingLayout']) ? json['recordingLayout'] : RecordingLayout.BEST_FIT; this.properties.recordingLayout = !!(json['recordingLayout']) ? json['recordingLayout'] : RecordingLayout.BEST_FIT;
if (this.properties.recordingLayout.toString() === RecordingLayout[RecordingLayout.CUSTOM]) { if (this.properties.recordingLayout.toString() === RecordingLayout[RecordingLayout.CUSTOM]) {
@ -140,6 +141,9 @@ export namespace Recording {
* Record all streams in a grid layout in a single archive * Record all streams in a grid layout in a single archive
*/ */
COMPOSED = 'COMPOSED', COMPOSED = 'COMPOSED',
COMPOSED_QUICK_START = 'COMPOSED_QUICK_START',
/** /**
* Record each stream individually * Record each stream individually

View File

@ -1,109 +1,130 @@
#!/bin/bash -x #!/bin/bash
### Variables ### # DEBUG MODE
DEBUG_MODE=${DEBUG_MODE:-false}
if [[ ${DEBUG_MODE} == true ]]; then
DEBUG_CHROME_FLAGS="--enable-logging --v=1"
fi
URL=${URL:-https://www.youtube.com/watch?v=JMuzlEQz3uo} {
ONLY_VIDEO=${ONLY_VIDEO:-false} ### Variables ###
RESOLUTION=${RESOLUTION:-1920x1080}
FRAMERATE=${FRAMERATE:-25}
WIDTH="$(cut -d'x' -f1 <<< $RESOLUTION)"
HEIGHT="$(cut -d'x' -f2 <<< $RESOLUTION)"
VIDEO_ID=${VIDEO_ID:-video}
VIDEO_NAME=${VIDEO_NAME:-video}
VIDEO_FORMAT=${VIDEO_FORMAT:-mp4}
RECORDING_JSON="${RECORDING_JSON}"
export URL URL=${URL:-https://www.youtube.com/watch?v=JMuzlEQz3uo}
export ONLY_VIDEO ONLY_VIDEO=${ONLY_VIDEO:-false}
export RESOLUTION RESOLUTION=${RESOLUTION:-1920x1080}
export FRAMERATE FRAMERATE=${FRAMERATE:-25}
export WIDTH WIDTH="$(cut -d'x' -f1 <<< $RESOLUTION)"
export HEIGHT HEIGHT="$(cut -d'x' -f2 <<< $RESOLUTION)"
export VIDEO_ID VIDEO_ID=${VIDEO_ID:-video}
export VIDEO_NAME VIDEO_NAME=${VIDEO_NAME:-video}
export VIDEO_FORMAT VIDEO_FORMAT=${VIDEO_FORMAT:-mp4}
export RECORDING_JSON RECORDING_JSON="${RECORDING_JSON}"
### Store Recording json data ### export URL
export ONLY_VIDEO
export RESOLUTION
export FRAMERATE
export WIDTH
export HEIGHT
export VIDEO_ID
export VIDEO_NAME
export VIDEO_FORMAT
export RECORDING_JSON
echo "==== Loaded Environment Variables ======================="
env
echo "========================================================="
mkdir /recordings/$VIDEO_ID ### Store Recording json data ###
chmod 777 /recordings/$VIDEO_ID
echo $RECORDING_JSON > /recordings/$VIDEO_ID/.recording.$VIDEO_ID
### Get a free display identificator ### mkdir /recordings/$VIDEO_ID
chmod 777 /recordings/$VIDEO_ID
echo $RECORDING_JSON > /recordings/$VIDEO_ID/.recording.$VIDEO_ID
DISPLAY_NUM=99 ### Get a free display identificator ###
DONE="no"
while [ "$DONE" == "no" ] DISPLAY_NUM=99
do DONE="no"
out=$(xdpyinfo -display :$DISPLAY_NUM 2>&1)
if [[ "$out" == name* ]] || [[ "$out" == Invalid* ]] while [ "$DONE" == "no" ]
then do
# Command succeeded; or failed with access error; display exists out=$(xdpyinfo -display :$DISPLAY_NUM 2>&1)
(( DISPLAY_NUM+=1 )) if [[ "$out" == name* ]] || [[ "$out" == Invalid* ]]
else then
# Display doesn't exist # Command succeeded; or failed with access error; display exists
DONE="yes" (( DISPLAY_NUM+=1 ))
else
# Display doesn't exist
DONE="yes"
fi
done
export DISPLAY_NUM
echo "First available display -> :$DISPLAY_NUM"
echo "----------------------------------------"
pulseaudio -D
### Start Chrome in headless mode with xvfb, using the display num previously obtained ###
touch xvfb.log
chmod 777 xvfb.log
xvfb-run --server-num=${DISPLAY_NUM} --server-args="-ac -screen 0 ${RESOLUTION}x24 -noreset" google-chrome --kiosk --start-maximized --test-type --no-sandbox --disable-infobars --disable-gpu --disable-popup-blocking --window-size=$WIDTH,$HEIGHT --window-position=0,0 --no-first-run --ignore-certificate-errors --autoplay-policy=no-user-gesture-required $DEBUG_CHROME_FLAGS $URL &> xvfb.log &
touch stop
chmod 777 /recordings
sleep 2
### Start recording with ffmpeg ###
if [[ "$ONLY_VIDEO" == true ]]
then
# Do not record audio
<./stop ffmpeg -y -f x11grab -draw_mouse 0 -framerate $FRAMERATE -video_size $RESOLUTION -i :$DISPLAY_NUM -c:v libx264 -preset ultrafast -crf 28 -refs 4 -qmin 4 -pix_fmt yuv420p -filter:v fps=$FRAMERATE "/recordings/$VIDEO_ID/$VIDEO_NAME.$VIDEO_FORMAT"
else
# Record audio ("-f alsa -i pulse [...] -c:a aac")
<./stop ffmpeg -y -f alsa -i pulse -f x11grab -draw_mouse 0 -framerate $FRAMERATE -video_size $RESOLUTION -i :$DISPLAY_NUM -c:a aac -c:v libx264 -preset ultrafast -crf 28 -refs 4 -qmin 4 -pix_fmt yuv420p -filter:v fps=$FRAMERATE "/recordings/$VIDEO_ID/$VIDEO_NAME.$VIDEO_FORMAT"
fi fi
done
export DISPLAY_NUM ### Generate video report file ###
ffprobe -v quiet -print_format json -show_format -show_streams /recordings/$VIDEO_ID/$VIDEO_NAME.$VIDEO_FORMAT > /recordings/$VIDEO_ID/$VIDEO_ID.info
echo "First available display -> :$DISPLAY_NUM" ### Update Recording json data ###
echo "----------------------------------------"
pulseaudio -D TMP=$(mktemp /recordings/$VIDEO_ID/.$VIDEO_ID.XXXXXXXXXXXXXXXXXXXXXXX.json)
INFO=$(cat /recordings/$VIDEO_ID/$VIDEO_ID.info | jq '.')
HAS_AUDIO_AUX=$(echo $INFO | jq '.streams[] | select(.codec_type == "audio")')
if [ -z "$HAS_AUDIO_AUX" ]; then HAS_AUDIO=false; else HAS_AUDIO=true; fi
HAS_VIDEO_AUX=$(echo $INFO | jq '.streams[] | select(.codec_type == "video")')
if [ -z "$HAS_VIDEO_AUX" ]; then HAS_VIDEO=false; else HAS_VIDEO=true; fi
SIZE=$(echo $INFO | jq '.format.size | tonumber')
DURATION=$(echo $INFO | jq '.format.duration | tonumber')
### Start Chrome in headless mode with xvfb, using the display num previously obtained ### if [[ "$HAS_AUDIO" == false && "$HAS_VIDEO" == false ]]
then
STATUS="failed"
else
STATUS="stopped"
fi
touch xvfb.log jq -c -r ".hasAudio=$HAS_AUDIO | .hasVideo=$HAS_VIDEO | .duration=$DURATION | .size=$SIZE | .status=\"$STATUS\"" "/recordings/$VIDEO_ID/.recording.$VIDEO_ID" > $TMP && mv $TMP /recordings/$VIDEO_ID/.recording.$VIDEO_ID
chmod 777 xvfb.log
xvfb-run --server-num=${DISPLAY_NUM} --server-args="-ac -screen 0 ${RESOLUTION}x24 -noreset" google-chrome --kiosk --start-maximized --test-type --no-sandbox --disable-infobars --disable-gpu --disable-popup-blocking --window-size=$WIDTH,$HEIGHT --window-position=0,0 --no-first-run --ignore-certificate-errors --autoplay-policy=no-user-gesture-required $URL &> xvfb.log &
touch stop
chmod 777 /recordings
sleep 2
### Start recording with ffmpeg ### ### Generate video thumbnail ###
if [[ "$ONLY_VIDEO" == true ]] MIDDLE_TIME=$(ffmpeg -i /recordings/$VIDEO_ID/$VIDEO_NAME.$VIDEO_FORMAT 2>&1 | grep Duration | awk '{print $2}' | tr -d , | awk -F ':' '{print ($3+$2*60+$1*3600)/2}')
then THUMBNAIL_HEIGHT=$((480*$HEIGHT/$WIDTH))
# Do not record audio ffmpeg -ss $MIDDLE_TIME -i /recordings/$VIDEO_ID/$VIDEO_NAME.$VIDEO_FORMAT -vframes 1 -s 480x$THUMBNAIL_HEIGHT /recordings/$VIDEO_ID/$VIDEO_ID.jpg
<./stop ffmpeg -y -f x11grab -draw_mouse 0 -framerate $FRAMERATE -video_size $RESOLUTION -i :$DISPLAY_NUM -c:v libx264 -preset ultrafast -crf 28 -refs 4 -qmin 4 -pix_fmt yuv420p -filter:v fps=$FRAMERATE "/recordings/$VIDEO_ID/$VIDEO_NAME.$VIDEO_FORMAT"
else ### Change permissions to all generated files ###
# Record audio ("-f alsa -i pulse [...] -c:a aac")
<./stop ffmpeg -y -f alsa -i pulse -f x11grab -draw_mouse 0 -framerate $FRAMERATE -video_size $RESOLUTION -i :$DISPLAY_NUM -c:a aac -c:v libx264 -preset ultrafast -crf 28 -refs 4 -qmin 4 -pix_fmt yuv420p -filter:v fps=$FRAMERATE "/recordings/$VIDEO_ID/$VIDEO_NAME.$VIDEO_FORMAT" sudo chmod -R 777 /recordings/$VIDEO_ID
} 2>&1 | tee -a /tmp/container.log
if [[ ${DEBUG_MODE} == "true" ]]; then
[[ -f /tmp/container.log ]] && cp /tmp/container.log /recordings/$VIDEO_ID/$VIDEO_ID-container.log || echo "/tmp/container.log not found"
[[ -f ~/.config/google-chrome/chrome_debug.log ]] && cp ~/.config/google-chrome/chrome_debug.log /recordings/$VIDEO_ID/chrome_debug.log || echo "~/.config/google-chrome/chrome_debug.log"
fi fi
### Generate video report file ###
ffprobe -v quiet -print_format json -show_format -show_streams /recordings/$VIDEO_ID/$VIDEO_NAME.$VIDEO_FORMAT > /recordings/$VIDEO_ID/$VIDEO_ID.info
### Update Recording json data ###
TMP=$(mktemp /recordings/$VIDEO_ID/.$VIDEO_ID.XXXXXXXXXXXXXXXXXXXXXXX.json)
INFO=$(cat /recordings/$VIDEO_ID/$VIDEO_ID.info | jq '.')
HAS_AUDIO_AUX=$(echo $INFO | jq '.streams[] | select(.codec_type == "audio")')
if [ -z "$HAS_AUDIO_AUX" ]; then HAS_AUDIO=false; else HAS_AUDIO=true; fi
HAS_VIDEO_AUX=$(echo $INFO | jq '.streams[] | select(.codec_type == "video")')
if [ -z "$HAS_VIDEO_AUX" ]; then HAS_VIDEO=false; else HAS_VIDEO=true; fi
SIZE=$(echo $INFO | jq '.format.size | tonumber')
DURATION=$(echo $INFO | jq '.format.duration | tonumber')
if [[ "$HAS_AUDIO" == false && "$HAS_VIDEO" == false ]]
then
STATUS="failed"
else
STATUS="stopped"
fi
jq -c -r ".hasAudio=$HAS_AUDIO | .hasVideo=$HAS_VIDEO | .duration=$DURATION | .size=$SIZE | .status=\"$STATUS\"" "/recordings/$VIDEO_ID/.recording.$VIDEO_ID" > $TMP && mv $TMP /recordings/$VIDEO_ID/.recording.$VIDEO_ID
### Generate video thumbnail ###
MIDDLE_TIME=$(ffmpeg -i /recordings/$VIDEO_ID/$VIDEO_NAME.$VIDEO_FORMAT 2>&1 | grep Duration | awk '{print $2}' | tr -d , | awk -F ':' '{print ($3+$2*60+$1*3600)/2}')
THUMBNAIL_HEIGHT=$((480*$HEIGHT/$WIDTH))
ffmpeg -ss $MIDDLE_TIME -i /recordings/$VIDEO_ID/$VIDEO_NAME.$VIDEO_FORMAT -vframes 1 -s 480x$THUMBNAIL_HEIGHT /recordings/$VIDEO_ID/$VIDEO_ID.jpg
### Change permissions to all generated files ### ### Change permissions to all generated files ###
sudo chmod -R 777 /recordings/$VIDEO_ID sudo chmod -R 777 /recordings/$VIDEO_ID

View File

@ -1,141 +1,183 @@
#!/bin/bash -x #!/bin/bash
### Global variables ###
RESOLUTION=${RESOLUTION:-1920x1080}
WIDTH="$(cut -d'x' -f1 <<< $RESOLUTION)"
HEIGHT="$(cut -d'x' -f2 <<< $RESOLUTION)"
export RESOLUTION
export WIDTH
export HEIGHT
# DEBUG MODE
# If debug mode
DEBUG_MODE=${DEBUG_MODE:-false}
if [[ ${DEBUG_MODE} == true ]]; then
DEBUG_CHROME_FLAGS="--enable-logging --v=1"
fi
# QUICK_START_ACTION indicates wich action to perform when COMPOSED_QUICK_START mode is executed # QUICK_START_ACTION indicates wich action to perform when COMPOSED_QUICK_START mode is executed
# Possible values are: # Possible values are:
# - Without parameters: Just execute all necessary configuration for xfvb and start chrome, waiting forever with a session openned # - Without parameters: Just execute all necessary configuration for xfvb and start chrome, waiting forever with a session openned
# - --start-recording: Executes ffmpeg to record a session but don't stop chrome # - --start-recording: Executes ffmpeg to record a session but don't stop chrome
# - --stop-recording: Stops ffmpeg recording # - --process-recording: Process ffmpeg video and generates a metadata
COMPOSED_QUICK_START_ACTION=$1 export COMPOSED_QUICK_START_ACTION=$1
export COMPOSED_QUICK_START_ACTION
if [[ -z "${COMPOSED_QUICK_START_ACTION}" ]]; then if [[ -z "${COMPOSED_QUICK_START_ACTION}" ]]; then
{
### Variables ###
export RESOLUTION=${RESOLUTION:-1920x1080}
export URL=${URL:-https://www.youtube.com/watch?v=JMuzlEQz3uo}
export VIDEO_ID=${VIDEO_ID:-video}
export WIDTH="$(cut -d'x' -f1 <<< $RESOLUTION)"
export HEIGHT="$(cut -d'x' -f2 <<< $RESOLUTION)"
export RECORDING_MODE=${RECORDING_MODE}
### Get a free display identificator ###
DISPLAY_NUM=99
DONE="no"
echo "====== Loaded Environment Variables - Start Chrome ======"
env
echo "========================================================="
while [ "$DONE" == "no" ]
do
out=$(xdpyinfo -display :$DISPLAY_NUM 2>&1)
if [[ "$out" == name* ]] || [[ "$out" == Invalid* ]]
then
# Command succeeded; or failed with access error; display exists
(( DISPLAY_NUM+=1 ))
else
# Display doesn't exist
DONE="yes"
fi
done
export DISPLAY_NUM
echo "First available display -> :$DISPLAY_NUM"
echo "----------------------------------------"
pulseaudio -D
### Start Chrome in headless mode with xvfb, using the display num previously obtained ###
touch xvfb.log
chmod 777 xvfb.log
xvfb-run --server-num=${DISPLAY_NUM} --server-args="-ac -screen 0 ${RESOLUTION}x24 -noreset" google-chrome --kiosk --start-maximized --test-type --no-sandbox --disable-infobars --disable-gpu --disable-popup-blocking --window-size=$WIDTH,$HEIGHT --window-position=0,0 --no-first-run --ignore-certificate-errors --autoplay-policy=no-user-gesture-required --enable-logging --v=1 $DEBUG_CHROME_FLAGS $URL &> xvfb.log &
chmod 777 /recordings
### Variables ### # Save Global Environment variables
URL=${URL:-https://www.youtube.com/watch?v=JMuzlEQz3uo} echo "export DISPLAY_NUM=$DISPLAY_NUM" > /tmp/display_num
export URL
### Get a free display identificator ### } 2>&1 | tee -a /tmp/container-start.log
DISPLAY_NUM=99
DONE="no"
while [ "$DONE" == "no" ]
do
out=$(xdpyinfo -display :$DISPLAY_NUM 2>&1)
if [[ "$out" == name* ]] || [[ "$out" == Invalid* ]]
then
# Command succeeded; or failed with access error; display exists
(( DISPLAY_NUM+=1 ))
else
# Display doesn't exist
DONE="yes"
fi
done
export DISPLAY_NUM
# Save DISPLAY_NUM in a temp file to be accessible for other scripts
echo "export DISPLAY_NUM=${DISPLAY_NUM}" >> /tmp/DISPLAY_NUM
echo "First available display -> :$DISPLAY_NUM"
echo "----------------------------------------"
pulseaudio -D
### Start Chrome in headless mode with xvfb, using the display num previously obtained ###
touch xvfb.log
chmod 777 xvfb.log
xvfb-run --server-num=${DISPLAY_NUM} --server-args="-ac -screen 0 ${RESOLUTION}x24 -noreset" google-chrome --kiosk --start-maximized --test-type --no-sandbox --disable-infobars --disable-gpu --disable-popup-blocking --window-size=$WIDTH,$HEIGHT --window-position=0,0 --no-first-run --ignore-certificate-errors --autoplay-policy=no-user-gesture-required $URL &> xvfb.log &
chmod 777 /recordings
sleep infinity sleep infinity
elif [[ "${COMPOSED_QUICK_START_ACTION}" == "--start-recording" ]]; then elif [[ "${COMPOSED_QUICK_START_ACTION}" == "--start-recording" ]]; then
{
export $(cat /tmp/display_num | xargs)
rm -f /tmp/global_environment_vars
# Remove possible stop file from previous recordings
[ -e stop ] && rm stop
# Create stop file
touch stop
source /tmp/DISPLAY_NUM # Variables
export RESOLUTION=${RESOLUTION:-1920x1080}
# Remove possible stop file from previous recordings export WIDTH="$(cut -d'x' -f1 <<< $RESOLUTION)"
[ -e stop ] && rm stop export HEIGHT="$(cut -d'x' -f2 <<< $RESOLUTION)"
# Create stop file export ONLY_VIDEO=${ONLY_VIDEO:-false}
touch stop export FRAMERATE=${FRAMERATE:-25}
export VIDEO_ID=${VIDEO_ID:-video}
export VIDEO_NAME=${VIDEO_NAME:-video}
export VIDEO_FORMAT=${VIDEO_FORMAT:-mp4}
export RECORDING_JSON="${RECORDING_JSON}"
echo "==== Loaded Environment Variables - Start Recording ====="
env
echo "========================================================="
### Store Recording json data ###
# Variables mkdir /recordings/$VIDEO_ID
ONLY_VIDEO=${ONLY_VIDEO:-false} echo $RECORDING_JSON > /recordings/$VIDEO_ID/.recording.$VIDEO_ID
FRAMERATE=${FRAMERATE:-25} chmod 777 -R /recordings/$VIDEO_ID
VIDEO_ID=${VIDEO_ID:-video}
VIDEO_NAME=${VIDEO_NAME:-video} # Save Global Environment variables
VIDEO_FORMAT=${VIDEO_FORMAT:-mp4} env > /tmp/global_environment_vars
RECORDING_JSON="${RECORDING_JSON}"
### Start recording with ffmpeg ###
export ONLY_VIDEO
export FRAMERATE
export VIDEO_ID
export VIDEO_NAME
export VIDEO_FORMAT
export RECORDING_JSON
### Store Recording json data ###
mkdir /recordings/$VIDEO_ID if [[ "$ONLY_VIDEO" == true ]]
chmod 777 /recordings/$VIDEO_ID then
echo $RECORDING_JSON > /recordings/$VIDEO_ID/.recording.$VIDEO_ID # Do not record audio
<./stop ffmpeg -y -f x11grab -draw_mouse 0 -framerate $FRAMERATE -video_size $RESOLUTION -i :$DISPLAY_NUM -c:v libx264 -preset ultrafast -crf 28 -refs 4 -qmin 4 -pix_fmt yuv420p -filter:v fps=$FRAMERATE "/recordings/$VIDEO_ID/$VIDEO_NAME.$VIDEO_FORMAT"
### Start recording with ffmpeg ### else
# Record audio ("-f alsa -i pulse [...] -c:a aac")
if [[ "$ONLY_VIDEO" == true ]] <./stop ffmpeg -y -f alsa -i pulse -f x11grab -draw_mouse 0 -framerate $FRAMERATE -video_size $RESOLUTION -i :$DISPLAY_NUM -c:a aac -c:v libx264 -preset ultrafast -crf 28 -refs 4 -qmin 4 -pix_fmt yuv420p -filter:v fps=$FRAMERATE "/recordings/$VIDEO_ID/$VIDEO_NAME.$VIDEO_FORMAT"
then fi
# Do not record audio
<./stop ffmpeg -y -f x11grab -draw_mouse 0 -framerate $FRAMERATE -video_size $RESOLUTION -i :$DISPLAY_NUM -c:v libx264 -preset ultrafast -crf 28 -refs 4 -qmin 4 -pix_fmt yuv420p -filter:v fps=$FRAMERATE "/recordings/$VIDEO_ID/$VIDEO_NAME.$VIDEO_FORMAT" } 2>&1 | tee -a /tmp/container-start-recording.log
else
# Record audio ("-f alsa -i pulse [...] -c:a aac")
<./stop ffmpeg -y -f alsa -i pulse -f x11grab -draw_mouse 0 -framerate $FRAMERATE -video_size $RESOLUTION -i :$DISPLAY_NUM -c:a aac -c:v libx264 -preset ultrafast -crf 28 -refs 4 -qmin 4 -pix_fmt yuv420p -filter:v fps=$FRAMERATE "/recordings/$VIDEO_ID/$VIDEO_NAME.$VIDEO_FORMAT"
fi
### Generate video report file ###
ffprobe -v quiet -print_format json -show_format -show_streams /recordings/$VIDEO_ID/$VIDEO_NAME.$VIDEO_FORMAT > /recordings/$VIDEO_ID/$VIDEO_ID.info
### Update Recording json data ###
TMP=$(mktemp /recordings/$VIDEO_ID/.$VIDEO_ID.XXXXXXXXXXXXXXXXXXXXXXX.json)
INFO=$(cat /recordings/$VIDEO_ID/$VIDEO_ID.info | jq '.')
HAS_AUDIO_AUX=$(echo $INFO | jq '.streams[] | select(.codec_type == "audio")')
if [ -z "$HAS_AUDIO_AUX" ]; then HAS_AUDIO=false; else HAS_AUDIO=true; fi
HAS_VIDEO_AUX=$(echo $INFO | jq '.streams[] | select(.codec_type == "video")')
if [ -z "$HAS_VIDEO_AUX" ]; then HAS_VIDEO=false; else HAS_VIDEO=true; fi
SIZE=$(echo $INFO | jq '.format.size | tonumber')
DURATION=$(echo $INFO | jq '.format.duration | tonumber')
if [[ "$HAS_AUDIO" == false && "$HAS_VIDEO" == false ]]
then
STATUS="failed"
else
STATUS="stopped"
fi
jq -c -r ".hasAudio=$HAS_AUDIO | .hasVideo=$HAS_VIDEO | .duration=$DURATION | .size=$SIZE | .status=\"$STATUS\"" "/recordings/$VIDEO_ID/.recording.$VIDEO_ID" > $TMP && mv $TMP /recordings/$VIDEO_ID/.recording.$VIDEO_ID
### Generate video thumbnail ###
MIDDLE_TIME=$(ffmpeg -i /recordings/$VIDEO_ID/$VIDEO_NAME.$VIDEO_FORMAT 2>&1 | grep Duration | awk '{print $2}' | tr -d , | awk -F ':' '{print ($3+$2*60+$1*3600)/2}')
THUMBNAIL_HEIGHT=$((480*$HEIGHT/$WIDTH))
ffmpeg -ss $MIDDLE_TIME -i /recordings/$VIDEO_ID/$VIDEO_NAME.$VIDEO_FORMAT -vframes 1 -s 480x$THUMBNAIL_HEIGHT /recordings/$VIDEO_ID/$VIDEO_ID.jpg
### Change permissions to all generated files ###
sudo chmod -R 777 /recordings/$VIDEO_ID
elif [[ "${COMPOSED_QUICK_START_ACTION}" == "--stop-recording" ]]; then elif [[ "${COMPOSED_QUICK_START_ACTION}" == "--stop-recording" ]]; then
echo 'q' > stop
{
# Load global variables saved before
export $(cat /tmp/global_environment_vars | xargs)
if [[ -f /recordings/$VIDEO_ID/$VIDEO_ID.jpg ]]; then
echo "Video already recorded"
exit 0
fi
# Stop and wait ffmpeg process to be stopped
FFMPEG_PID=$(pgrep ffmpeg)
echo 'q' > stop && tail --pid=$FFMPEG_PID -f /dev/null
### Generate video report file ###
ffprobe -v quiet -print_format json -show_format -show_streams /recordings/$VIDEO_ID/$VIDEO_NAME.$VIDEO_FORMAT > /recordings/$VIDEO_ID/$VIDEO_ID.info
### Change permissions to all generated files ###
sudo chmod -R 777 /recordings/$VIDEO_ID
### Update Recording json data ###
TMP=$(mktemp /recordings/$VIDEO_ID/.$VIDEO_ID.XXXXXXXXXXXXXXXXXXXXXXX.json)
INFO=$(cat /recordings/$VIDEO_ID/$VIDEO_ID.info | jq '.')
HAS_AUDIO_AUX=$(echo $INFO | jq '.streams[] | select(.codec_type == "audio")')
if [ -z "$HAS_AUDIO_AUX" ]; then HAS_AUDIO=false; else HAS_AUDIO=true; fi
HAS_VIDEO_AUX=$(echo $INFO | jq '.streams[] | select(.codec_type == "video")')
if [ -z "$HAS_VIDEO_AUX" ]; then HAS_VIDEO=false; else HAS_VIDEO=true; fi
SIZE=$(echo $INFO | jq '.format.size | tonumber')
DURATION=$(echo $INFO | jq '.format.duration | tonumber')
if [[ "$HAS_AUDIO" == false && "$HAS_VIDEO" == false ]]
then
STATUS="failed"
else
STATUS="stopped"
fi
jq -c -r ".hasAudio=$HAS_AUDIO | .hasVideo=$HAS_VIDEO | .duration=$DURATION | .size=$SIZE | .status=\"$STATUS\"" "/recordings/$VIDEO_ID/.recording.$VIDEO_ID" > $TMP && mv $TMP /recordings/$VIDEO_ID/.recording.$VIDEO_ID
rm -f $TMP
### Change permissions to metadata file ###
sudo chmod 777 /recordings/$VIDEO_ID/.recording.$VIDEO_ID
echo "Recording finished /recordings/$VIDEO_ID/$VIDEO_NAME.$VIDEO_FORMAT"
### Generate video thumbnail ###
MIDDLE_TIME=$(ffmpeg -i /recordings/$VIDEO_ID/$VIDEO_NAME.$VIDEO_FORMAT 2>&1 | grep Duration | awk '{print $2}' | tr -d , | awk -F ':' '{print ($3+$2*60+$1*3600)/2}')
THUMBNAIL_HEIGHT=$((480*$HEIGHT/$WIDTH))
ffmpeg -ss $MIDDLE_TIME -i /recordings/$VIDEO_ID/$VIDEO_NAME.$VIDEO_FORMAT -vframes 1 -s 480x$THUMBNAIL_HEIGHT /recordings/$VIDEO_ID/$VIDEO_ID.jpg &> /tmp/ffmpeg-thumbnail.log
} 2>&1 | tee -a /tmp/container-stop-recording.log
fi fi
if [[ ${DEBUG_MODE} == "true" ]]; then
[[ -f /tmp/container-start.log ]] && cp /tmp/container-start.log /recordings/$VIDEO_ID/$VIDEO_ID-container-start.log || echo "/tmp/container-start.log not found"
[[ -f /tmp/container-start-recording.log ]] && cp /tmp/container-start-recording.log /recordings/$VIDEO_ID/$VIDEO_ID-container-start-recording.log || echo "/tmp/container-start-recording.log not found"
[[ -f /tmp/container-stop-recording.log ]] && cp /tmp/container-stop-recording.log /recordings/$VIDEO_ID/$VIDEO_ID-container-stop-recording.log || echo "/tmp/container-stop-recording.log not found"
[[ -f ~/.config/google-chrome/chrome_debug.log ]] && cp ~/.config/google-chrome/chrome_debug.log /recordings/$VIDEO_ID/chrome_debug.log || echo "~/.config/google-chrome/chrome_debug.log"
fi
### Change permissions to all generated files ###
sudo chmod -R 777 /recordings/$VIDEO_ID

View File

@ -118,6 +118,8 @@ public class OpenviduConfig {
private boolean openviduRecording; private boolean openviduRecording;
private boolean openViduRecordingDebug;
private boolean openviduRecordingPublicAccess; private boolean openviduRecordingPublicAccess;
private Integer openviduRecordingAutostopTimeout; private Integer openviduRecordingAutostopTimeout;
@ -224,6 +226,10 @@ public class OpenviduConfig {
return this.openviduRecording; return this.openviduRecording;
} }
public boolean isOpenViduRecordingDebug() {
return openViduRecordingDebug;
}
public String getOpenViduRecordingPath() { public String getOpenViduRecordingPath() {
return this.openviduRecordingPath; return this.openviduRecordingPath;
} }
@ -476,6 +482,7 @@ public class OpenviduConfig {
: asFileSystemPath("OPENVIDU_CDR_PATH"); : asFileSystemPath("OPENVIDU_CDR_PATH");
openviduRecording = asBoolean("OPENVIDU_RECORDING"); openviduRecording = asBoolean("OPENVIDU_RECORDING");
openViduRecordingDebug = asBoolean("OPENVIDU_RECORDING_DEBUG");
openviduRecordingPath = openviduRecording ? asWritableFileSystemPath("OPENVIDU_RECORDING_PATH") openviduRecordingPath = openviduRecording ? asWritableFileSystemPath("OPENVIDU_RECORDING_PATH")
: asFileSystemPath("OPENVIDU_RECORDING_PATH"); : asFileSystemPath("OPENVIDU_RECORDING_PATH");
openviduRecordingPublicAccess = asBoolean("OPENVIDU_RECORDING_PUBLIC_ACCESS"); openviduRecordingPublicAccess = asBoolean("OPENVIDU_RECORDING_PUBLIC_ACCESS");

View File

@ -32,6 +32,7 @@ import java.util.stream.Collectors;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy; import javax.annotation.PreDestroy;
import io.openvidu.java.client.Recording;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.kurento.jsonrpc.message.Request; import org.kurento.jsonrpc.message.Request;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -545,10 +546,19 @@ public abstract class SessionManager {
if (openviduConfig.isRecordingModuleEnabled() && stopRecording if (openviduConfig.isRecordingModuleEnabled() && stopRecording
&& this.recordingManager.sessionIsBeingRecorded(session.getSessionId())) { && this.recordingManager.sessionIsBeingRecorded(session.getSessionId())) {
try { try {
recordingManager.stopRecording(session, null, RecordingManager.finalReason(reason)); recordingManager.stopRecording(session, null, RecordingManager.finalReason(reason), true);
} catch (OpenViduException e) { } catch (OpenViduException e) {
log.error("Error stopping recording of session {}: {}", session.getSessionId(), e.getMessage()); log.error("Error stopping recording of session {}: {}", session.getSessionId(), e.getMessage());
} }
} else if(openviduConfig.isRecordingModuleEnabled() && stopRecording
&& !this.recordingManager.sessionIsBeingRecorded(session.getSessionId())
&& session.getSessionProperties().defaultOutputMode().equals(Recording.OutputMode.COMPOSED_QUICK_START)
&& this.recordingManager.getStartedRecording(session.getSessionId()) != null) {
try {
this.recordingManager.stopComposedQuickStartContainer(session, reason);
} catch (OpenViduException e) {
log.error("Error stopping COMPOSED_QUICK_START container of session {}", session.getSessionId());
}
} }
final String mediaNodeId = session.getMediaNodeId(); final String mediaNodeId = session.getMediaNodeId();

View File

@ -30,6 +30,7 @@ import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import io.openvidu.java.client.*;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.kurento.client.GenericMediaElement; import org.kurento.client.GenericMediaElement;
import org.kurento.client.IceCandidate; import org.kurento.client.IceCandidate;
@ -47,11 +48,6 @@ import com.google.gson.JsonObject;
import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException;
import io.openvidu.client.OpenViduException.Code; import io.openvidu.client.OpenViduException.Code;
import io.openvidu.client.internal.ProtocolElements; import io.openvidu.client.internal.ProtocolElements;
import io.openvidu.java.client.MediaMode;
import io.openvidu.java.client.RecordingLayout;
import io.openvidu.java.client.RecordingMode;
import io.openvidu.java.client.RecordingProperties;
import io.openvidu.java.client.SessionProperties;
import io.openvidu.server.core.EndReason; import io.openvidu.server.core.EndReason;
import io.openvidu.server.core.FinalUser; import io.openvidu.server.core.FinalUser;
import io.openvidu.server.core.IdentifierPrefixes; import io.openvidu.server.core.IdentifierPrefixes;
@ -144,6 +140,12 @@ public class KurentoSessionManager extends SessionManager {
} }
} }
// If Recording default layout is COMPOSED_QUICK_START
Recording.OutputMode defaultOutputMode = kSession.getSessionProperties().defaultOutputMode();
if (defaultOutputMode.equals(Recording.OutputMode.COMPOSED_QUICK_START)) {
recordingManager.startComposedQuickStartContainer(kSession);
}
if (kSession.isClosed()) { if (kSession.isClosed()) {
log.warn("'{}' is trying to join session '{}' but it is closing", participant.getParticipantPublicId(), log.warn("'{}' is trying to join session '{}' but it is closing", participant.getParticipantPublicId(),
sessionId); sessionId);
@ -214,12 +216,12 @@ public class KurentoSessionManager extends SessionManager {
Participant p = sessionidParticipantpublicidParticipant.get(sessionId) Participant p = sessionidParticipantpublicidParticipant.get(sessionId)
.remove(participant.getParticipantPublicId()); .remove(participant.getParticipantPublicId());
if (this.openviduConfig.isTurnadminAvailable()) { if (p != null && this.openviduConfig.isTurnadminAvailable()) {
this.coturnCredentialsService.deleteUser(p.getToken().getTurnCredentials().getUsername()); this.coturnCredentialsService.deleteUser(p.getToken().getTurnCredentials().getUsername());
} }
// TODO: why is this necessary?? // TODO: why is this necessary??
if (insecureUsers.containsKey(p.getParticipantPrivateId())) { if (p != null && insecureUsers.containsKey(p.getParticipantPrivateId())) {
boolean stillParticipant = false; boolean stillParticipant = false;
for (Session s : sessions.values()) { for (Session s : sessions.values()) {
if (!s.isClosed() if (!s.isClosed()
@ -295,6 +297,14 @@ public class KurentoSessionManager extends SessionManager {
"Last participant left. Starting {} seconds countdown for stopping recording of session {}", "Last participant left. Starting {} seconds countdown for stopping recording of session {}",
this.openviduConfig.getOpenviduRecordingAutostopTimeout(), sessionId); this.openviduConfig.getOpenviduRecordingAutostopTimeout(), sessionId);
recordingManager.initAutomaticRecordingStopThread(session); recordingManager.initAutomaticRecordingStopThread(session);
} else if (remainingParticipants.size() == 1 && openviduConfig.isRecordingModuleEnabled()
&& MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode())
&& session.getSessionProperties().defaultOutputMode().equals(Recording.OutputMode.COMPOSED_QUICK_START)
&& ProtocolElements.RECORDER_PARTICIPANT_PUBLICID
.equals(remainingParticipants.iterator().next().getParticipantPublicId())) {
// If no recordings are active in COMPOSED_QUICK_START output mode, stop container
recordingManager.stopComposedQuickStartContainer(session, reason);
} }
} }

View File

@ -0,0 +1,232 @@
package io.openvidu.server.recording.service;
import com.github.dockerjava.api.model.Bind;
import com.github.dockerjava.api.model.Volume;
import io.openvidu.client.OpenViduException;
import io.openvidu.java.client.RecordingProperties;
import io.openvidu.server.cdr.CallDetailRecord;
import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.core.EndReason;
import io.openvidu.server.core.Session;
import io.openvidu.server.recording.Recording;
import io.openvidu.server.recording.RecordingDownloader;
import io.openvidu.server.utils.QuarantineKiller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
public class ComposedQuickStartRecordingService extends ComposedRecordingService {
private static final Logger log = LoggerFactory.getLogger(ComposedRecordingService.class);
public ComposedQuickStartRecordingService(RecordingManager recordingManager, RecordingDownloader recordingDownloader, OpenviduConfig openviduConfig, CallDetailRecord cdr, QuarantineKiller quarantineKiller) {
super(recordingManager, recordingDownloader, openviduConfig, cdr, quarantineKiller);
}
public void stopRecordingContainer(Session session, EndReason reason) {
log.info("Stopping COMPOSED_QUICK_START of session {}. Reason: {}",
session.getSessionId(), RecordingManager.finalReason(reason));
String containerId = this.sessionsContainers.get(session.getSessionId());
try {
dockerManager.removeDockerContainer(containerId, true);
} catch (Exception e) {
log.error("Can't remove COMPOSED_QUICK_START recording container from session {}", session.getSessionId());
}
containers.remove(containerId);
sessionsContainers.remove(session.getSessionId());
}
@Override
protected Recording startRecordingWithVideo(Session session, Recording recording, RecordingProperties properties)
throws OpenViduException {
log.info("Starting COMPOSED_QUICK_START ({}) recording {} of session {}",
properties.hasAudio() ? "video + audio" : "audio-only", recording.getId(), recording.getSessionId());
List<String> envs = new ArrayList<>();
envs.add("DEBUG_MODE=" + openviduConfig.isOpenViduRecordingDebug());
envs.add("RESOLUTION=" + properties.resolution());
envs.add("ONLY_VIDEO=" + !properties.hasAudio());
envs.add("FRAMERATE=30");
envs.add("VIDEO_ID=" + recording.getId());
envs.add("VIDEO_NAME=" + properties.name());
envs.add("VIDEO_FORMAT=mp4");
envs.add("RECORDING_JSON='" + recording.toJson().toString() + "'");
String containerId = this.sessionsContainers.get(session.getSessionId());
try {
String recordExecCommand = "";
for(int i = 0; i < envs.size(); i++) {
if (i > 0) {
recordExecCommand += "&& ";
}
recordExecCommand += "export " + envs.get(i) + " ";
}
recordExecCommand += "&& ./composed_quick_start.sh --start-recording > /var/log/ffmpeg.log 2>&1 &";
dockerManager.runCommandInContainer(containerId, recordExecCommand, 0);
} catch (Exception e) {
this.cleanRecordingMaps(recording);
throw this.failStartRecording(session, recording,
"Couldn't initialize recording container. Error: " + e.getMessage());
}
this.sessionsContainers.put(session.getSessionId(), containerId);
try {
this.waitForVideoFileNotEmpty(recording);
} catch (OpenViduException e) {
this.cleanRecordingMaps(recording);
throw this.failStartRecording(session, recording,
"Couldn't initialize recording container. Error: " + e.getMessage());
}
return recording;
}
@Override
protected Recording stopRecordingWithVideo(Session session, Recording recording, EndReason reason, boolean hasSessionEnded) {
log.info("Stopping COMPOSED_QUICK_START ({}) recording {} of session {}. Reason: {}",
recording.hasAudio() ? "video + audio" : "audio-only", recording.getId(), recording.getSessionId(),
RecordingManager.finalReason(reason));
log.info("Container for session {} still being ready for new recordings", session.getSessionId());
String containerId = this.sessionsContainers.get(recording.getSessionId());
if (session == null) {
log.warn(
"Existing recording {} does not have an active session associated. This usually means a custom recording"
+ " layout did not join a recorded participant or the recording has been automatically"
+ " stopped after last user left and timeout passed",
recording.getId());
}
if (hasSessionEnded) {
// Gracefully stop ffmpeg process
try {
dockerManager.runCommandInContainer(containerId, "./composed_quick_start.sh --stop-recording", 10);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
try {
dockerManager.removeDockerContainer(containerId, true);
} catch (Exception e) {
failRecordingCompletion(recording, containerId, new OpenViduException(OpenViduException.Code.RECORDING_COMPLETION_ERROR_CODE,
"Can't remove COMPOSED_QUICK_START recording container from session" + session.getSessionId()));
}
containers.remove(containerId);
sessionsContainers.remove(recording.getSessionId());
} else {
try {
dockerManager.runCommandInContainer(containerId, "./composed_quick_start.sh --stop-recording", 10);
} catch (InterruptedException e1) {
cleanRecordingMaps(recording);
log.error("Error stopping recording for session id: {}", session.getSessionId());
e1.printStackTrace();
}
}
recording = updateRecordingAttributes(recording);
final String folderPath = this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/";
final String metadataFilePath = folderPath + RecordingManager.RECORDING_ENTITY_FILE + recording.getId();
this.sealRecordingMetadataFileAsReady(recording, recording.getSize(), recording.getDuration(),
metadataFilePath);
cleanRecordingMaps(recording);
final long timestamp = System.currentTimeMillis();
this.cdr.recordRecordingStatusChanged(recording, reason, timestamp, recording.getStatus());
if (session != null && reason != null) {
this.recordingManager.sessionHandler.sendRecordingStoppedNotification(session, recording, reason);
}
// Decrement active recordings
// ((KurentoSession) session).getKms().getActiveRecordings().decrementAndGet();
return recording;
}
public void runComposedQuickStartContainer(Session session) {
// Start recording container if output mode=COMPOSED_QUICK_START
Session recorderSession = session;
io.openvidu.java.client.Recording.OutputMode defaultOutputMode = recorderSession.getSessionProperties().defaultOutputMode();
if (io.openvidu.java.client.Recording.OutputMode.COMPOSED_QUICK_START.equals(defaultOutputMode)
&& sessionsContainers.get(recorderSession.getSessionId()) == null) {
// Retry to run if container is launched for the same session quickly after close it
int secondsToRetry = 10;
int secondsBetweenRetries = 1;
int seconds = 0;
boolean launched = false;
while (!launched && seconds < secondsToRetry) {
try {
log.info("Launching COMPOSED_QUICK_START recording container for session: {}", recorderSession.getSessionId());
runContainer(recorderSession, new RecordingProperties.Builder().name("")
.outputMode(recorderSession.getSessionProperties().defaultOutputMode())
.recordingLayout(recorderSession.getSessionProperties().defaultRecordingLayout())
.customLayout(recorderSession.getSessionProperties().defaultCustomLayout()).build());
log.info("COMPOSED_QUICK_START recording container launched for session: {}", recorderSession.getSessionId());
launched = true;
} catch (Exception e) {
log.warn("Failed to launch COMPOSED_QUICK_START recording container for session {}. Trying again in {} seconds", recorderSession.getSessionId(), secondsBetweenRetries);
try {
Thread.sleep(secondsBetweenRetries * 1000);
} catch (InterruptedException e2) {}
seconds++;
} finally {
if (seconds == secondsToRetry && !launched) {
log.error("Error launchaing COMPOSED_QUICK_ªSTART recording container for session {}", recorderSession.getSessionId());
}
}
}
}
}
private String runContainer(Session session, RecordingProperties properties) throws Exception {
log.info("Starting COMPOSED_QUICK_START container for session id: {}", session.getSessionId());
Recording recording = new Recording(session.getSessionId(), null, properties);
String layoutUrl = this.getLayoutUrl(recording);
List<String> envs = new ArrayList<>();
envs.add("DEBUG_MODE=" + openviduConfig.isOpenViduRecordingDebug());
envs.add("RECORDING_TYPE=COMPOSED_QUICK_START");
envs.add("RESOLUTION=" + properties.resolution());
envs.add("URL=" + layoutUrl);
log.info("Recorder connecting to url {}", layoutUrl);
String containerId = null;
try {
final String container = RecordingManager.IMAGE_NAME + ":" + RecordingManager.IMAGE_TAG;
final String containerName = "recording_" + session.getSessionId();
Volume volume1 = new Volume("/recordings");
List<Volume> volumes = new ArrayList<>();
volumes.add(volume1);
Bind bind1 = new Bind(openviduConfig.getOpenViduRecordingPath(), volume1);
List<Bind> binds = new ArrayList<>();
binds.add(bind1);
containerId = dockerManager.runContainer(container, containerName, null, volumes, binds, "host", envs, null,
properties.shmSize(), false, null);
containers.put(containerId, containerName);
this.sessionsContainers.put(session.getSessionId(), containerId);
} catch (Exception e) {
if (containerId != null) {
dockerManager.removeDockerContainer(containerId, true);
containers.remove(containerId);
sessionsContainers.remove(session.getSessionId());
}
log.error("Error while launchig container for COMPOSED_QUICK_START: ({})", e.getMessage());
throw e;
}
return containerId;
}
}

View File

@ -63,11 +63,11 @@ public class ComposedRecordingService extends RecordingService {
private static final Logger log = LoggerFactory.getLogger(ComposedRecordingService.class); private static final Logger log = LoggerFactory.getLogger(ComposedRecordingService.class);
private Map<String, String> containers = new ConcurrentHashMap<>(); protected Map<String, String> containers = new ConcurrentHashMap<>();
private Map<String, String> sessionsContainers = new ConcurrentHashMap<>(); protected Map<String, String> sessionsContainers = new ConcurrentHashMap<>();
private Map<String, CompositeWrapper> composites = new ConcurrentHashMap<>(); private Map<String, CompositeWrapper> composites = new ConcurrentHashMap<>();
private DockerManager dockerManager; protected DockerManager dockerManager;
public ComposedRecordingService(RecordingManager recordingManager, RecordingDownloader recordingDownloader, public ComposedRecordingService(RecordingManager recordingManager, RecordingDownloader recordingDownloader,
OpenviduConfig openviduConfig, CallDetailRecord cdr, QuarantineKiller quarantineKiller) { OpenviduConfig openviduConfig, CallDetailRecord cdr, QuarantineKiller quarantineKiller) {
@ -102,18 +102,18 @@ public class ComposedRecordingService extends RecordingService {
} }
@Override @Override
public Recording stopRecording(Session session, Recording recording, EndReason reason) { public Recording stopRecording(Session session, Recording recording, EndReason reason, boolean hasSessionEnded) {
recording = this.sealRecordingMetadataFileAsStopped(recording); recording = this.sealRecordingMetadataFileAsStopped(recording);
if (recording.hasVideo()) { if (recording.hasVideo()) {
return this.stopRecordingWithVideo(session, recording, reason); return this.stopRecordingWithVideo(session, recording, reason, hasSessionEnded);
} else { } else {
return this.stopRecordingAudioOnly(session, recording, reason, 0); return this.stopRecordingAudioOnly(session, recording, reason, 0);
} }
} }
public Recording stopRecording(Session session, Recording recording, EndReason reason, long kmsDisconnectionTime) { public Recording stopRecording(Session session, Recording recording, EndReason reason, long kmsDisconnectionTime, boolean hasSessionEnded) {
if (recording.hasVideo()) { if (recording.hasVideo()) {
return this.stopRecordingWithVideo(session, recording, reason); return this.stopRecordingWithVideo(session, recording, reason, hasSessionEnded);
} else { } else {
return this.stopRecordingAudioOnly(session, recording, reason, kmsDisconnectionTime); return this.stopRecordingAudioOnly(session, recording, reason, kmsDisconnectionTime);
} }
@ -142,7 +142,7 @@ public class ComposedRecordingService extends RecordingService {
compositeWrapper.disconnectPublisherEndpoint(streamId); compositeWrapper.disconnectPublisherEndpoint(streamId);
} }
private Recording startRecordingWithVideo(Session session, Recording recording, RecordingProperties properties) protected Recording startRecordingWithVideo(Session session, Recording recording, RecordingProperties properties)
throws OpenViduException { throws OpenViduException {
log.info("Starting composed ({}) recording {} of session {}", log.info("Starting composed ({}) recording {} of session {}",
@ -152,6 +152,7 @@ public class ComposedRecordingService extends RecordingService {
String layoutUrl = this.getLayoutUrl(recording); String layoutUrl = this.getLayoutUrl(recording);
envs.add("DEBUG_MODE=" + openviduConfig.isOpenViduRecordingDebug());
envs.add("URL=" + layoutUrl); envs.add("URL=" + layoutUrl);
envs.add("ONLY_VIDEO=" + !properties.hasAudio()); envs.add("ONLY_VIDEO=" + !properties.hasAudio());
envs.add("RESOLUTION=" + properties.resolution()); envs.add("RESOLUTION=" + properties.resolution());
@ -227,7 +228,7 @@ public class ComposedRecordingService extends RecordingService {
return recording; return recording;
} }
private Recording stopRecordingWithVideo(Session session, Recording recording, EndReason reason) { protected Recording stopRecordingWithVideo(Session session, Recording recording, EndReason reason, boolean hasSessionEnded) {
log.info("Stopping composed ({}) recording {} of session {}. Reason: {}", log.info("Stopping composed ({}) recording {} of session {}. Reason: {}",
recording.hasAudio() ? "video + audio" : "audio-only", recording.getId(), recording.getSessionId(), recording.hasAudio() ? "video + audio" : "audio-only", recording.getId(), recording.getSessionId(),
@ -389,7 +390,7 @@ public class ComposedRecordingService extends RecordingService {
return finalRecordingArray[0]; return finalRecordingArray[0];
} }
private void stopAndRemoveRecordingContainer(Recording recording, String containerId, int secondsOfWait) { protected void stopAndRemoveRecordingContainer(Recording recording, String containerId, int secondsOfWait) {
// Gracefully stop ffmpeg process // Gracefully stop ffmpeg process
try { try {
dockerManager.runCommandInContainer(containerId, "echo 'q' > stop", 0); dockerManager.runCommandInContainer(containerId, "echo 'q' > stop", 0);
@ -411,7 +412,7 @@ public class ComposedRecordingService extends RecordingService {
containers.remove(containerId); containers.remove(containerId);
} }
private Recording updateRecordingAttributes(Recording recording) { protected Recording updateRecordingAttributes(Recording recording) {
try { try {
RecordingInfoUtils infoUtils = new RecordingInfoUtils(this.openviduConfig.getOpenViduRecordingPath() RecordingInfoUtils infoUtils = new RecordingInfoUtils(this.openviduConfig.getOpenViduRecordingPath()
+ recording.getId() + "/" + recording.getId() + ".info"); + recording.getId() + "/" + recording.getId() + ".info");
@ -436,7 +437,7 @@ public class ComposedRecordingService extends RecordingService {
} }
} }
private void waitForVideoFileNotEmpty(Recording recording) throws OpenViduException { protected void waitForVideoFileNotEmpty(Recording recording) throws OpenViduException {
boolean isPresent = false; boolean isPresent = false;
int i = 1; int i = 1;
int timeout = 150; // Wait for 150*150 = 22500 = 22.5 seconds int timeout = 150; // Wait for 150*150 = 22500 = 22.5 seconds
@ -459,7 +460,7 @@ public class ComposedRecordingService extends RecordingService {
} }
} }
private void failRecordingCompletion(Recording recording, String containerId, OpenViduException e) protected void failRecordingCompletion(Recording recording, String containerId, OpenViduException e)
throws OpenViduException { throws OpenViduException {
recording.setStatus(io.openvidu.java.client.Recording.Status.failed); recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
dockerManager.removeDockerContainer(containerId, true); dockerManager.removeDockerContainer(containerId, true);
@ -467,7 +468,7 @@ public class ComposedRecordingService extends RecordingService {
throw e; throw e;
} }
private String getLayoutUrl(Recording recording) throws OpenViduException { protected String getLayoutUrl(Recording recording) throws OpenViduException {
String secret = openviduConfig.getOpenViduSecret(); String secret = openviduConfig.getOpenViduSecret();
// Check if "customLayout" property defines a final URL // Check if "customLayout" property defines a final URL

View File

@ -76,12 +76,14 @@ import io.openvidu.server.utils.CustomFileManager;
import io.openvidu.server.utils.DockerManager; import io.openvidu.server.utils.DockerManager;
import io.openvidu.server.utils.JsonUtils; import io.openvidu.server.utils.JsonUtils;
import io.openvidu.server.utils.QuarantineKiller; import io.openvidu.server.utils.QuarantineKiller;
import org.springframework.http.ResponseEntity;
public class RecordingManager { public class RecordingManager {
private static final Logger log = LoggerFactory.getLogger(RecordingManager.class); private static final Logger log = LoggerFactory.getLogger(RecordingManager.class);
private ComposedRecordingService composedRecordingService; private ComposedRecordingService composedRecordingService;
private ComposedQuickStartRecordingService composedQuickStartRecordingService;
private SingleStreamRecordingService singleStreamRecordingService; private SingleStreamRecordingService singleStreamRecordingService;
private DockerManager dockerManager; private DockerManager dockerManager;
@ -159,6 +161,8 @@ public class RecordingManager {
this.dockerManager = new DockerManager(); this.dockerManager = new DockerManager();
this.composedRecordingService = new ComposedRecordingService(this, recordingDownloader, openviduConfig, cdr, this.composedRecordingService = new ComposedRecordingService(this, recordingDownloader, openviduConfig, cdr,
quarantineKiller); quarantineKiller);
this.composedQuickStartRecordingService = new ComposedQuickStartRecordingService(this, recordingDownloader, openviduConfig, cdr,
quarantineKiller);
this.singleStreamRecordingService = new SingleStreamRecordingService(this, recordingDownloader, openviduConfig, this.singleStreamRecordingService = new SingleStreamRecordingService(this, recordingDownloader, openviduConfig,
cdr, quarantineKiller); cdr, quarantineKiller);
@ -231,6 +235,14 @@ public class RecordingManager {
this.checkRecordingPaths(openviduRecordingPath, openviduRecordingCustomLayout); this.checkRecordingPaths(openviduRecordingPath, openviduRecordingCustomLayout);
} }
public void startComposedQuickStartContainer(Session session) {
this.composedQuickStartRecordingService.runComposedQuickStartContainer(session);
}
public void stopComposedQuickStartContainer(Session session, EndReason reason) {
this.composedQuickStartRecordingService.stopRecordingContainer(session, reason);
}
public Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException { public Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException {
try { try {
if (session.recordingLock.tryLock(15, TimeUnit.SECONDS)) { if (session.recordingLock.tryLock(15, TimeUnit.SECONDS)) {
@ -245,6 +257,9 @@ public class RecordingManager {
case COMPOSED: case COMPOSED:
recording = this.composedRecordingService.startRecording(session, properties); recording = this.composedRecordingService.startRecording(session, properties);
break; break;
case COMPOSED_QUICK_START:
recording = this.composedQuickStartRecordingService.startRecording(session, properties);
break;
case INDIVIDUAL: case INDIVIDUAL:
recording = this.singleStreamRecordingService.startRecording(session, properties); recording = this.singleStreamRecordingService.startRecording(session, properties);
break; break;
@ -287,7 +302,7 @@ public class RecordingManager {
} }
} }
public Recording stopRecording(Session session, String recordingId, EndReason reason) { public Recording stopRecording(Session session, String recordingId, EndReason reason, boolean hasSessionEnded) {
Recording recording; Recording recording;
if (session == null) { if (session == null) {
recording = this.startedRecordings.get(recordingId); recording = this.startedRecordings.get(recordingId);
@ -301,10 +316,13 @@ public class RecordingManager {
switch (recording.getOutputMode()) { switch (recording.getOutputMode()) {
case COMPOSED: case COMPOSED:
recording = this.composedRecordingService.stopRecording(session, recording, reason); recording = this.composedRecordingService.stopRecording(session, recording, reason, hasSessionEnded);
break;
case COMPOSED_QUICK_START:
recording = this.composedQuickStartRecordingService.stopRecording(session, recording, reason, hasSessionEnded);
break; break;
case INDIVIDUAL: case INDIVIDUAL:
recording = this.singleStreamRecordingService.stopRecording(session, recording, reason); recording = this.singleStreamRecordingService.stopRecording(session, recording, reason, hasSessionEnded);
break; break;
} }
this.abortAutomaticRecordingStopThread(session, reason); this.abortAutomaticRecordingStopThread(session, reason);
@ -316,7 +334,16 @@ public class RecordingManager {
recording = this.sessionsRecordings.get(session.getSessionId()); recording = this.sessionsRecordings.get(session.getSessionId());
switch (recording.getOutputMode()) { switch (recording.getOutputMode()) {
case COMPOSED: case COMPOSED:
recording = this.composedRecordingService.stopRecording(session, recording, reason, kmsDisconnectionTime); recording = this.composedRecordingService.stopRecording(session, recording, reason, kmsDisconnectionTime, true);
if (recording.hasVideo()) {
// Evict the recorder participant if composed recording with video
this.sessionManager.evictParticipant(
session.getParticipantByPublicId(ProtocolElements.RECORDER_PARTICIPANT_PUBLICID), null, null,
null);
}
break;
case COMPOSED_QUICK_START:
recording = this.composedQuickStartRecordingService.stopRecording(session, recording, reason, kmsDisconnectionTime, true);
if (recording.hasVideo()) { if (recording.hasVideo()) {
// Evict the recorder participant if composed recording with video // Evict the recorder participant if composed recording with video
this.sessionManager.evictParticipant( this.sessionManager.evictParticipant(
@ -531,7 +558,7 @@ public class RecordingManager {
log.info( log.info(
"Automatic stopping recording {}. There are users connected to session {}, but no one is publishing", "Automatic stopping recording {}. There are users connected to session {}, but no one is publishing",
recordingId, session.getSessionId()); recordingId, session.getSessionId());
this.stopRecording(session, recordingId, EndReason.automaticStop); this.stopRecording(session, recordingId, EndReason.automaticStop, true);
} }
} finally { } finally {
if (!alreadyUnlocked) { if (!alreadyUnlocked) {

View File

@ -58,7 +58,7 @@ public abstract class RecordingService {
public abstract Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException; public abstract Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException;
public abstract Recording stopRecording(Session session, Recording recording, EndReason reason); public abstract Recording stopRecording(Session session, Recording recording, EndReason reason, boolean hasSessionEnded);
/** /**
* Generates metadata recording file (".recording.RECORDING_ID" JSON file to * Generates metadata recording file (".recording.RECORDING_ID" JSON file to
@ -177,7 +177,7 @@ public abstract class RecordingService {
recording.setStatus(io.openvidu.java.client.Recording.Status.failed); recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
this.recordingManager.startingRecordings.remove(recording.getId()); this.recordingManager.startingRecordings.remove(recording.getId());
this.recordingManager.sessionsRecordingsStarting.remove(session.getSessionId()); this.recordingManager.sessionsRecordingsStarting.remove(session.getSessionId());
this.stopRecording(session, recording, null); this.stopRecording(session, recording, null, true);
return new OpenViduException(Code.RECORDING_START_ERROR_CODE, errorMessage); return new OpenViduException(Code.RECORDING_START_ERROR_CODE, errorMessage);
} }

View File

@ -138,7 +138,7 @@ public class SingleStreamRecordingService extends RecordingService {
} }
@Override @Override
public Recording stopRecording(Session session, Recording recording, EndReason reason) { public Recording stopRecording(Session session, Recording recording, EndReason reason, boolean hasSessionEnded) {
recording = this.sealRecordingMetadataFileAsStopped(recording); recording = this.sealRecordingMetadataFileAsStopped(recording);
return this.stopRecording(session, recording, reason, 0); return this.stopRecording(session, recording, reason, 0);
} }

View File

@ -597,7 +597,7 @@ public class SessionRestController {
Session session = sessionManager.getSession(recording.getSessionId()); Session session = sessionManager.getSession(recording.getSessionId());
Recording stoppedRecording = this.recordingManager.stopRecording(session, recording.getId(), Recording stoppedRecording = this.recordingManager.stopRecording(session, recording.getId(),
EndReason.recordingStoppedByServer); EndReason.recordingStoppedByServer, false);
session.recordingManuallyStopped.set(true); session.recordingManuallyStopped.set(true);

View File

@ -46,6 +46,12 @@
"description": "Whether to start OpenVidu Server with recording module service available or not (a Docker image will be downloaded during the first execution). Apart from setting this param to true, it is also necessary to explicitly configure sessions to be recorded", "description": "Whether to start OpenVidu Server with recording module service available or not (a Docker image will be downloaded during the first execution). Apart from setting this param to true, it is also necessary to explicitly configure sessions to be recorded",
"defaultValue": false "defaultValue": false
}, },
{
"name": "OPENVIDU_RECORDING_DEBUG",
"type": "java.lang.Boolean",
"description": "If true, start recording service in debug mode",
"defaultValue": false
},
{ {
"name": "OPENVIDU_RECORDING_PATH", "name": "OPENVIDU_RECORDING_PATH",
"type": "java.lang.String", "type": "java.lang.String",

View File

@ -25,6 +25,7 @@ OPENVIDU_WEBHOOK_HEADERS=[]
OPENVIDU_WEBHOOK_EVENTS=["sessionCreated","sessionDestroyed","participantJoined","participantLeft","webrtcConnectionCreated","webrtcConnectionDestroyed","recordingStatusChanged","filterEventDispatched","mediaNodeStatusChanged"] OPENVIDU_WEBHOOK_EVENTS=["sessionCreated","sessionDestroyed","participantJoined","participantLeft","webrtcConnectionCreated","webrtcConnectionDestroyed","recordingStatusChanged","filterEventDispatched","mediaNodeStatusChanged"]
OPENVIDU_RECORDING=false OPENVIDU_RECORDING=false
OPENVIDU_RECORDING_DEBUG=false
OPENVIDU_RECORDING_VERSION=2.9.0 OPENVIDU_RECORDING_VERSION=2.9.0
OPENVIDU_RECORDING_PATH=/opt/openvidu/recordings OPENVIDU_RECORDING_PATH=/opt/openvidu/recordings
OPENVIDU_RECORDING_PUBLIC_ACCESS=false OPENVIDU_RECORDING_PUBLIC_ACCESS=false

File diff suppressed because it is too large Load Diff

View File

@ -1,48 +1,48 @@
{ {
"dependencies": { "dependencies": {
"@angular/animations": "8.2.14", "@angular/animations": "8.2.14",
"@angular/cdk": "8.2.3", "@angular/cdk": "8.2.3",
"@angular/common": "8.2.14", "@angular/common": "8.2.14",
"@angular/compiler": "8.2.14", "@angular/compiler": "8.2.14",
"@angular/core": "8.2.14", "@angular/core": "8.2.14",
"@angular/flex-layout": "8.0.0-beta.27", "@angular/flex-layout": "8.0.0-beta.27",
"@angular/forms": "8.2.14", "@angular/forms": "8.2.14",
"@angular/http": "7.2.15", "@angular/http": "7.2.15",
"@angular/material": "8.2.3", "@angular/material": "8.2.3",
"@angular/platform-browser": "8.2.14", "@angular/platform-browser": "8.2.14",
"@angular/platform-browser-dynamic": "8.2.14", "@angular/platform-browser-dynamic": "8.2.14",
"@angular/router": "8.2.14", "@angular/router": "8.2.14",
"colormap": "2.3.1", "colormap": "2.3.1",
"core-js": "3.4.7", "core-js": "3.4.7",
"hammerjs": "2.0.8", "hammerjs": "2.0.8",
"openvidu-browser": "2.14.0", "openvidu-browser": "2.14.0",
"openvidu-node-client": "2.11.0", "openvidu-node-client": "2.11.0",
"rxjs": "6.5.3", "rxjs": "6.5.3",
"zone.js": "0.10.2" "zone.js": "0.10.2"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "0.803.20", "@angular-devkit/build-angular": "0.803.20",
"@angular/cli": "8.3.20", "@angular/cli": "8.3.20",
"@angular/compiler-cli": "8.2.14", "@angular/compiler-cli": "8.2.14",
"@angular/language-service": "8.2.14", "@angular/language-service": "8.2.14",
"@types/jasmine": "3.5.0", "@types/jasmine": "3.5.0",
"@types/jasminewd2": "2.0.8", "@types/jasminewd2": "2.0.8",
"@types/node": "12.12.14", "@types/node": "12.12.14",
"codelyzer": "5.2.0", "codelyzer": "5.2.0",
"ts-node": "8.5.4", "ts-node": "8.5.4",
"tslint": "5.20.1", "tslint": "5.20.1",
"typescript": "3.5.3" "typescript": "3.5.3"
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"name": "openvidu-testapp", "name": "openvidu-testapp",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "ng build", "build": "ng build",
"e2e": "ng e2e", "e2e": "ng e2e",
"lint": "ng lint", "lint": "ng lint",
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
"test": "ng test" "test": "ng test"
}, },
"version": "2.14.0" "version": "2.14.0"
} }