Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 91 additions & 20 deletions ServerRecording/Controller/CallRecordingController.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from azure.eventgrid import EventGridEvent
from azure.eventgrid._generated.models import SubscriptionValidationEventData, \
AcsRecordingFileStatusUpdatedEventData, AcsRecordingChunkInfoProperties
from BlobStorageHelper import BlobStorageHelper
from ConfigurationManager import ConfigurationManager
from Logger import Logger
import json
import ast
from aiohttp import web
from azure.communication.callingserver import CallingServerClient, ServerCallLocator
from Root import Root
from FileFormats import FileFormat, Mapper
from ServerRecording.FileFormats import DownloadType

CALL_RECORDING_ACTIVE_ERROR_CODE = "8553"
CALL_RECODING_NOT_FOUND_ERROR_CODE = "8522"
Expand All @@ -31,10 +32,14 @@

class CallRecordingController():

recFileFormat = ''

def __init__(self):
app = web.Application()
app.add_routes(
[web.get('/startRecording', CallRecordingController.start_recording)])
app.add_routes(
[web.get('/startRecordingWithOptions', CallRecordingController.start_recording_with_options)])
app.add_routes(
[web.get('/pauseRecording', CallRecordingController.pause_recording)])
app.add_routes(
Expand Down Expand Up @@ -83,6 +88,58 @@ async def start_recording(request):

return web.Response(text=str(ex), status=500)

async def start_recording_with_options(request):
try:
server_call_id = request.rel_url.query['serverCallId']

if not server_call_id:
return web.Response(text="serverCallId is invalid", status=400)

server_content_type = request.rel_url.query['recordingContent']
server_channel_type = request.rel_url.query['recordingChannel']
server_format_type = request.rel_url.query['recordingFormat']

Logger.log_message(
Logger.INFORMATION,
'StartRecording with channel called with serverCallId --> ' +
server_call_id + ' ,recordingContent --> '
+ server_content_type + ' ,recordingChannel ---> ' + server_channel_type + ' ,recordingFormat ---> ' + server_format_type)

call_locator = ServerCallLocator(server_call_id)
mapper = Mapper()
content_type = server_content_type if server_content_type in mapper.rec_content else 'audiovideo'
channel_type = server_channel_type if server_channel_type in mapper.rec_channel else 'mixed'
format_type = server_format_type if server_format_type in mapper.rec_format else 'mp4'

#Passing RecordingContent initiates recording in specific format. audio/audiovideo
#RecordingChannel is used to pass the channel type. mixed/unmixed
#RecordingFormat is used to pass the format of the recording. mp4/mp3/wav

res = calling_server_client.start_recording(call_locator=call_locator,
recording_state_callback_uri=call_back_uri,
recording_content_type=content_type,
recording_channel_type=channel_type,
recording_format_type=format_type)

Logger.log_message(
Logger.INFORMATION,
"StartRecording with channel response --> " + str(res) + ", Recording Id: " + res.recording_id)

if server_call_id not in recording_data.keys():
recording_data[server_call_id] = ''
recording_data[server_call_id] = res.recording_id

return web.Response(text=res.recording_id)
except Exception as ex:
Logger.log_message(
Logger.ERROR, "Failed to start server recording with channel info--> " + str(ex))
if CALL_RECORDING_ACTIVE_ERROR_CODE in str(ex) or \
INVALID_JOIN_IDENTITY_ERROR_CODE in str(ex) or \
CALL_NOT_ESTABLISHED_ERROR_CODE in str(ex):
return web.Response(text=str(ex), status=400)

return web.Response(text=str(ex), status=500)

async def pause_recording(request):
try:
server_call_id = request.rel_url.query['serverCallId']
Expand Down Expand Up @@ -222,7 +279,7 @@ async def get_recording_file(request):
try:
if event.event_type == 'Microsoft.EventGrid.SubscriptionValidationEvent':
try:
subscription_validation_event: SubscriptionValidationEventData = event_data
subscription_validation_event = event_data
code = subscription_validation_event['validationCode']
if code:
data = {"validationResponse": code}
Expand All @@ -235,10 +292,9 @@ async def get_recording_file(request):
return web.Response(text=str(ex), status=500)

if event.event_type == 'Microsoft.Communication.RecordingFileStatusUpdated':
acs_recording_file_status_updated_event_data: AcsRecordingFileStatusUpdatedEventData = event_data
acs_recording_chunk_info_properties: AcsRecordingChunkInfoProperties = \
acs_recording_file_status_updated_event_data[
'recordingStorageInfo']['recordingChunks'][0]
acs_recording_file_status_updated_event_data = event_data
acs_recording_chunk_info_properties = acs_recording_file_status_updated_event_data[
'recordingStorageInfo']['recordingChunks'][0]

Logger.log_message(
Logger.INFORMATION, "acsRecordingChunkInfoProperties response data --> " + str(acs_recording_chunk_info_properties))
Expand All @@ -247,31 +303,34 @@ async def get_recording_file(request):
content_location = acs_recording_chunk_info_properties['contentLocation']
metadata_location = acs_recording_chunk_info_properties['metadataLocation']

process_recording_response = CallRecordingController.process_file(
process_metadata_response = CallRecordingController.process_file(
document_id,
content_location,
'mp4',
'recording')
metadata_location,
FileFormat.json,
DownloadType.METADATA)

if process_recording_response is True:
if process_metadata_response is True:
Logger.log_message(
Logger.INFORMATION, "Processing metadata file completed successfully.")
Logger.log_message(
Logger.INFORMATION, "Start processing metadata -- >")
Logger.INFORMATION, "Start processing recording file -- >")

process_metadata_response = CallRecordingController.process_file(
process_recording_response = CallRecordingController.process_file(
document_id,
metadata_location,
'json',
'metadata')
content_location,
CallRecordingController.recFileFormat,
DownloadType.RECORDING)

if process_metadata_response is True:
if process_recording_response is True:
Logger.log_message(
Logger.INFORMATION, "Processing recording and metadata files completed successfully.")
else:
Logger.log_message(
Logger.INFORMATION, "Processing metadata file failed with message --> " + str(process_metadata_response))
Logger.INFORMATION, "Processing recording file failed with message --> " + str(process_recording_response))

else:
Logger.log_message(
Logger.INFORMATION, "Processing recording file failed with message --> " + str(process_recording_response))
Logger.INFORMATION, "Processing metadata file failed with message --> " + str(process_metadata_response))

except Exception as ex:
Logger.log_message(
Expand Down Expand Up @@ -303,6 +362,18 @@ def process_file(document_id: str, download_location: str, file_format: str, dow
except Exception as ex:
rec_file.close()

if download_type == DownloadType.METADATA:
with open(file_name) as f:
deserializedFile = json.load(f)
obj = Root(**deserializedFile)

format = obj.recordingInfo['format'] if obj.recordingInfo[
'format'] in Mapper.rec_format else FileFormat.mp4
CallRecordingController.recFileFormat = format

Logger.log_message(
Logger.INFORMATION, "Recording File Format is -- > " + CallRecordingController.recFileFormat)

upload_response = BlobStorageHelper.upload_file_to_storage(
container_name=container_name,
blob_name=file_name,
Expand Down
17 changes: 17 additions & 0 deletions ServerRecording/FileFormats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class Mapper:

rec_content = ['audio', 'audiovideo']
rec_channel = ['mixed', 'unmixed']
rec_format = ['mp3', 'mp4', 'wav']


class FileFormat:
json = 'json'
mp4 = 'mp4'
mp3 = 'mp3'
wav = 'wav'


class DownloadType:
RECORDING = 'RECORDING'
METADATA = 'METADATA'
72 changes: 72 additions & 0 deletions ServerRecording/Root.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from uuid import UUID
from datetime import datetime
from typing import List, Any


class Participant:
participantId: str

def __init__(self, participantId: str) -> None:
self.participantId = participantId


class AudioConfiguration:
sampleRate: int
bitRate: int
channels: int

def __init__(self, sampleRate: int, bitRate: int, channels: int) -> None:
self.sampleRate = sampleRate
self.bitRate = bitRate
self.channels = channels


class VideoConfiguration:
longerSideLength: int
shorterSideLength: int
framerate: int
bitRate: int

def __init__(self, longerSideLength: int, shorterSideLength: int, framerate: int, bitRate: int) -> None:
self.longerSideLength = longerSideLength
self.shorterSideLength = shorterSideLength
self.framerate = framerate
self.bitRate = bitRate


class RecordingInfo:
contentType: str
channelType: str
format: str
audioConfiguration: AudioConfiguration
videoConfiguration: VideoConfiguration

def __init__(self, contentType: str, channelType: str, format: str, audioConfiguration: AudioConfiguration, videoConfiguration: VideoConfiguration) -> None:
self.contentType = contentType
self.channelType = channelType
self.format = format
self.audioConfiguration = audioConfiguration
self.videoConfiguration = videoConfiguration


class Root:
resourceId: UUID
callId: UUID
chunkDocumentId: str
chunkIndex: int
chunkStartTime: datetime
chunkDuration: float
pauseResumeIntervals: List[Any]
recordingInfo: RecordingInfo
participants: List[Participant]

def __init__(self, resourceId: UUID, callId: UUID, chunkDocumentId: str, chunkIndex: int, chunkStartTime: datetime, chunkDuration: float, pauseResumeIntervals: List[Any], recordingInfo: RecordingInfo, participants: List[Participant]) -> None:
self.resourceId = resourceId
self.callId = callId
self.chunkDocumentId = chunkDocumentId
self.chunkIndex = chunkIndex
self.chunkStartTime = chunkStartTime
self.chunkDuration = chunkDuration
self.pauseResumeIntervals = pauseResumeIntervals
self.recordingInfo = recordingInfo
self.participants = participants