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("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("recordingLayout",
(properties.recordingLayout() != null) ? properties.recordingLayout().name() : "");

View File

@ -74,7 +74,14 @@ public class Recording {
/**
* 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;
@ -105,7 +112,7 @@ public class Recording {
OutputMode outputMode = OutputMode.valueOf(json.get("outputMode").getAsString());
RecordingProperties.Builder builder = new RecordingProperties.Builder().name(json.get("name").getAsString())
.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.recordingLayout(RecordingLayout.valueOf(json.get("recordingLayout").getAsString()));
JsonElement customLayout = json.get("customLayout");

View File

@ -52,7 +52,7 @@ public class RecordingProperties {
* Builder for {@link io.openvidu.java.client.RecordingProperties}
*/
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.resolution = this.resolution != null ? this.resolution : "1920x1080";
if (RecordingLayout.CUSTOM.equals(this.recordingLayout)) {

View File

@ -23,6 +23,7 @@ import { RecordingProperties } from './RecordingProperties';
import { Session } from './Session';
import { SessionProperties } from './SessionProperties';
import { RecordingLayout } from './RecordingLayout';
import { RecordingMode } from 'RecordingMode';
/**
* @hidden
@ -143,7 +144,8 @@ export class OpenVidu {
hasAudio: !!(properties.hasAudio),
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.recordingLayout = !!properties.recordingLayout ? properties.recordingLayout : RecordingLayout.BEST_FIT;
if (data.recordingLayout.toString() === RecordingLayout[RecordingLayout.CUSTOM]) {

View File

@ -82,7 +82,8 @@ export class Recording {
hasAudio: !!(json['hasAudio']),
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.recordingLayout = !!(json['recordingLayout']) ? json['recordingLayout'] : RecordingLayout.BEST_FIT;
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
*/
COMPOSED = 'COMPOSED',
COMPOSED_QUICK_START = 'COMPOSED_QUICK_START',
/**
* 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}
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}"
{
### Variables ###
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
URL=${URL:-https://www.youtube.com/watch?v=JMuzlEQz3uo}
ONLY_VIDEO=${ONLY_VIDEO:-false}
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}"
### 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
chmod 777 /recordings/$VIDEO_ID
echo $RECORDING_JSON > /recordings/$VIDEO_ID/.recording.$VIDEO_ID
### Store Recording json data ###
### 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
DONE="no"
### Get a free display identificator ###
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"
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
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
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"
echo "----------------------------------------"
### Update Recording json data ###
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
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
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
### Start recording with ffmpeg ###
### Generate video thumbnail ###
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"
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
} 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
### 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

View File

@ -1,141 +1,183 @@
#!/bin/bash -x
### Global variables ###
RESOLUTION=${RESOLUTION:-1920x1080}
WIDTH="$(cut -d'x' -f1 <<< $RESOLUTION)"
HEIGHT="$(cut -d'x' -f2 <<< $RESOLUTION)"
export RESOLUTION
export WIDTH
export HEIGHT
#!/bin/bash
# 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
# Possible values are:
# - 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
# - --stop-recording: Stops ffmpeg recording
COMPOSED_QUICK_START_ACTION=$1
export COMPOSED_QUICK_START_ACTION
# - --process-recording: Process ffmpeg video and generates a metadata
export COMPOSED_QUICK_START_ACTION=$1
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 ###
URL=${URL:-https://www.youtube.com/watch?v=JMuzlEQz3uo}
export URL
# Save Global Environment variables
echo "export DISPLAY_NUM=$DISPLAY_NUM" > /tmp/display_num
### Get a free display identificator ###
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
} 2>&1 | tee -a /tmp/container-start.log
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
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
# Remove possible stop file from previous recordings
[ -e stop ] && rm stop
# Create stop file
touch stop
# Variables
export RESOLUTION=${RESOLUTION:-1920x1080}
export WIDTH="$(cut -d'x' -f1 <<< $RESOLUTION)"
export HEIGHT="$(cut -d'x' -f2 <<< $RESOLUTION)"
export ONLY_VIDEO=${ONLY_VIDEO:-false}
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
ONLY_VIDEO=${ONLY_VIDEO:-false}
FRAMERATE=${FRAMERATE:-25}
VIDEO_ID=${VIDEO_ID:-video}
VIDEO_NAME=${VIDEO_NAME:-video}
VIDEO_FORMAT=${VIDEO_FORMAT:-mp4}
RECORDING_JSON="${RECORDING_JSON}"
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
echo $RECORDING_JSON > /recordings/$VIDEO_ID/.recording.$VIDEO_ID
chmod 777 -R /recordings/$VIDEO_ID
# Save Global Environment variables
env > /tmp/global_environment_vars
### Start recording with ffmpeg ###
mkdir /recordings/$VIDEO_ID
chmod 777 /recordings/$VIDEO_ID
echo $RECORDING_JSON > /recordings/$VIDEO_ID/.recording.$VIDEO_ID
### 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
### 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
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
} 2>&1 | tee -a /tmp/container-start-recording.log
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
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 openViduRecordingDebug;
private boolean openviduRecordingPublicAccess;
private Integer openviduRecordingAutostopTimeout;
@ -224,6 +226,10 @@ public class OpenviduConfig {
return this.openviduRecording;
}
public boolean isOpenViduRecordingDebug() {
return openViduRecordingDebug;
}
public String getOpenViduRecordingPath() {
return this.openviduRecordingPath;
}
@ -476,6 +482,7 @@ public class OpenviduConfig {
: asFileSystemPath("OPENVIDU_CDR_PATH");
openviduRecording = asBoolean("OPENVIDU_RECORDING");
openViduRecordingDebug = asBoolean("OPENVIDU_RECORDING_DEBUG");
openviduRecordingPath = openviduRecording ? asWritableFileSystemPath("OPENVIDU_RECORDING_PATH")
: asFileSystemPath("OPENVIDU_RECORDING_PATH");
openviduRecordingPublicAccess = asBoolean("OPENVIDU_RECORDING_PUBLIC_ACCESS");

View File

@ -32,6 +32,7 @@ import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import io.openvidu.java.client.Recording;
import org.apache.commons.lang3.RandomStringUtils;
import org.kurento.jsonrpc.message.Request;
import org.slf4j.Logger;
@ -545,10 +546,19 @@ public abstract class SessionManager {
if (openviduConfig.isRecordingModuleEnabled() && stopRecording
&& this.recordingManager.sessionIsBeingRecorded(session.getSessionId())) {
try {
recordingManager.stopRecording(session, null, RecordingManager.finalReason(reason));
recordingManager.stopRecording(session, null, RecordingManager.finalReason(reason), true);
} catch (OpenViduException e) {
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();

View File

@ -30,6 +30,7 @@ import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.openvidu.java.client.*;
import org.apache.commons.lang3.RandomStringUtils;
import org.kurento.client.GenericMediaElement;
import org.kurento.client.IceCandidate;
@ -47,11 +48,6 @@ import com.google.gson.JsonObject;
import io.openvidu.client.OpenViduException;
import io.openvidu.client.OpenViduException.Code;
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.FinalUser;
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()) {
log.warn("'{}' is trying to join session '{}' but it is closing", participant.getParticipantPublicId(),
sessionId);
@ -214,12 +216,12 @@ public class KurentoSessionManager extends SessionManager {
Participant p = sessionidParticipantpublicidParticipant.get(sessionId)
.remove(participant.getParticipantPublicId());
if (this.openviduConfig.isTurnadminAvailable()) {
if (p != null && this.openviduConfig.isTurnadminAvailable()) {
this.coturnCredentialsService.deleteUser(p.getToken().getTurnCredentials().getUsername());
}
// TODO: why is this necessary??
if (insecureUsers.containsKey(p.getParticipantPrivateId())) {
if (p != null && insecureUsers.containsKey(p.getParticipantPrivateId())) {
boolean stillParticipant = false;
for (Session s : sessions.values()) {
if (!s.isClosed()
@ -295,6 +297,14 @@ public class KurentoSessionManager extends SessionManager {
"Last participant left. Starting {} seconds countdown for stopping recording of session {}",
this.openviduConfig.getOpenviduRecordingAutostopTimeout(), sessionId);
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 Map<String, String> containers = new ConcurrentHashMap<>();
private Map<String, String> sessionsContainers = new ConcurrentHashMap<>();
protected Map<String, String> containers = new ConcurrentHashMap<>();
protected Map<String, String> sessionsContainers = new ConcurrentHashMap<>();
private Map<String, CompositeWrapper> composites = new ConcurrentHashMap<>();
private DockerManager dockerManager;
protected DockerManager dockerManager;
public ComposedRecordingService(RecordingManager recordingManager, RecordingDownloader recordingDownloader,
OpenviduConfig openviduConfig, CallDetailRecord cdr, QuarantineKiller quarantineKiller) {
@ -102,18 +102,18 @@ public class ComposedRecordingService extends RecordingService {
}
@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);
if (recording.hasVideo()) {
return this.stopRecordingWithVideo(session, recording, reason);
return this.stopRecordingWithVideo(session, recording, reason, hasSessionEnded);
} else {
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()) {
return this.stopRecordingWithVideo(session, recording, reason);
return this.stopRecordingWithVideo(session, recording, reason, hasSessionEnded);
} else {
return this.stopRecordingAudioOnly(session, recording, reason, kmsDisconnectionTime);
}
@ -142,7 +142,7 @@ public class ComposedRecordingService extends RecordingService {
compositeWrapper.disconnectPublisherEndpoint(streamId);
}
private Recording startRecordingWithVideo(Session session, Recording recording, RecordingProperties properties)
protected Recording startRecordingWithVideo(Session session, Recording recording, RecordingProperties properties)
throws OpenViduException {
log.info("Starting composed ({}) recording {} of session {}",
@ -152,6 +152,7 @@ public class ComposedRecordingService extends RecordingService {
String layoutUrl = this.getLayoutUrl(recording);
envs.add("DEBUG_MODE=" + openviduConfig.isOpenViduRecordingDebug());
envs.add("URL=" + layoutUrl);
envs.add("ONLY_VIDEO=" + !properties.hasAudio());
envs.add("RESOLUTION=" + properties.resolution());
@ -227,7 +228,7 @@ public class ComposedRecordingService extends RecordingService {
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: {}",
recording.hasAudio() ? "video + audio" : "audio-only", recording.getId(), recording.getSessionId(),
@ -389,7 +390,7 @@ public class ComposedRecordingService extends RecordingService {
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
try {
dockerManager.runCommandInContainer(containerId, "echo 'q' > stop", 0);
@ -411,7 +412,7 @@ public class ComposedRecordingService extends RecordingService {
containers.remove(containerId);
}
private Recording updateRecordingAttributes(Recording recording) {
protected Recording updateRecordingAttributes(Recording recording) {
try {
RecordingInfoUtils infoUtils = new RecordingInfoUtils(this.openviduConfig.getOpenViduRecordingPath()
+ 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;
int i = 1;
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 {
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
dockerManager.removeDockerContainer(containerId, true);
@ -467,7 +468,7 @@ public class ComposedRecordingService extends RecordingService {
throw e;
}
private String getLayoutUrl(Recording recording) throws OpenViduException {
protected String getLayoutUrl(Recording recording) throws OpenViduException {
String secret = openviduConfig.getOpenViduSecret();
// 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.JsonUtils;
import io.openvidu.server.utils.QuarantineKiller;
import org.springframework.http.ResponseEntity;
public class RecordingManager {
private static final Logger log = LoggerFactory.getLogger(RecordingManager.class);
private ComposedRecordingService composedRecordingService;
private ComposedQuickStartRecordingService composedQuickStartRecordingService;
private SingleStreamRecordingService singleStreamRecordingService;
private DockerManager dockerManager;
@ -159,6 +161,8 @@ public class RecordingManager {
this.dockerManager = new DockerManager();
this.composedRecordingService = new ComposedRecordingService(this, recordingDownloader, openviduConfig, cdr,
quarantineKiller);
this.composedQuickStartRecordingService = new ComposedQuickStartRecordingService(this, recordingDownloader, openviduConfig, cdr,
quarantineKiller);
this.singleStreamRecordingService = new SingleStreamRecordingService(this, recordingDownloader, openviduConfig,
cdr, quarantineKiller);
@ -231,6 +235,14 @@ public class RecordingManager {
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 {
try {
if (session.recordingLock.tryLock(15, TimeUnit.SECONDS)) {
@ -245,6 +257,9 @@ public class RecordingManager {
case COMPOSED:
recording = this.composedRecordingService.startRecording(session, properties);
break;
case COMPOSED_QUICK_START:
recording = this.composedQuickStartRecordingService.startRecording(session, properties);
break;
case INDIVIDUAL:
recording = this.singleStreamRecordingService.startRecording(session, properties);
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;
if (session == null) {
recording = this.startedRecordings.get(recordingId);
@ -301,10 +316,13 @@ public class RecordingManager {
switch (recording.getOutputMode()) {
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;
case INDIVIDUAL:
recording = this.singleStreamRecordingService.stopRecording(session, recording, reason);
recording = this.singleStreamRecordingService.stopRecording(session, recording, reason, hasSessionEnded);
break;
}
this.abortAutomaticRecordingStopThread(session, reason);
@ -316,7 +334,16 @@ public class RecordingManager {
recording = this.sessionsRecordings.get(session.getSessionId());
switch (recording.getOutputMode()) {
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()) {
// Evict the recorder participant if composed recording with video
this.sessionManager.evictParticipant(
@ -531,7 +558,7 @@ public class RecordingManager {
log.info(
"Automatic stopping recording {}. There are users connected to session {}, but no one is publishing",
recordingId, session.getSessionId());
this.stopRecording(session, recordingId, EndReason.automaticStop);
this.stopRecording(session, recordingId, EndReason.automaticStop, true);
}
} finally {
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 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
@ -177,7 +177,7 @@ public abstract class RecordingService {
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
this.recordingManager.startingRecordings.remove(recording.getId());
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);
}

View File

@ -138,7 +138,7 @@ public class SingleStreamRecordingService extends RecordingService {
}
@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);
return this.stopRecording(session, recording, reason, 0);
}

View File

@ -597,7 +597,7 @@ public class SessionRestController {
Session session = sessionManager.getSession(recording.getSessionId());
Recording stoppedRecording = this.recordingManager.stopRecording(session, recording.getId(),
EndReason.recordingStoppedByServer);
EndReason.recordingStoppedByServer, false);
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",
"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",
"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_RECORDING=false
OPENVIDU_RECORDING_DEBUG=false
OPENVIDU_RECORDING_VERSION=2.9.0
OPENVIDU_RECORDING_PATH=/opt/openvidu/recordings
OPENVIDU_RECORDING_PUBLIC_ACCESS=false

File diff suppressed because it is too large Load Diff

View File

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