microsoft / BotBuilder-RealTimeMediaCalling

BotBuilder-RealTimeMediaCalling extends the BotBuilder to enable bots to engage in Skype audio-video calling. It provides real-time, programmable access to the voice, video, and screen sharing streams of a Skype call. The bot is a direct participant in a Skype 1:1 call.
MIT License
76 stars 36 forks source link

How to save the audio conversation of the call? #19

Closed gfclaveria closed 7 years ago

gfclaveria commented 7 years ago

Hi

Do you have sample on how to record the audio of the conversation? Most specially the audio of each participant on the call conversation.

thanks,

waboum commented 7 years ago

Hi, If you wish to record audio, you will need to subscribe to the AudioMediaReceived Event m_audioSocket.AudioMediaReceived += OnAudioMediaReceived; This will be raised each time you have a received buffer.

private async void OnAudioMediaReceived(object sender, AudioMediaReceivedEventArgs e)
{
    var buffer = e.Buffer;
    try
    {
        if (m_wavFileWriter != null)
        {
            long length = buffer.Length;
            var retrievedBuffer = new byte[length];
            Marshal.Copy(buffer.Data, retrievedBuffer, 0, (int) length);
            await m_wavFileWriter.EnqueueAsync(retrievedBuffer);
        }
    }
    catch (Exception ex)
    {
        //log exception
    }
    finally
    {
        buffer.Dispose();
    }
}        

you can use this helper class to write to a wav file, this has dependency on the TPL dataflow nuget and System.Threading -First you need to create the writer

m_wavFileWriter = new WavFileWriter("WavOut" + DateTime.UtcNow.Ticks + ".wav",
                    new WavFileSettings());
               await m_wavFileWriter.InitializeAsync();

-Then, when the app is done recording call shutdown:

await m_wavFileWriter.ShutdownAsync();

This is the code for the helper class

 /// <summary>
    ///  wav file writer, this class will create a wav file
    ///  from the received buffers in the smart agents.
    /// </summary>
    internal class WavFileWriter
    {
        private BufferBlock<byte[]> m_queueBlock;
        private FileStream m_fileStream;
        private readonly CancellationTokenSource m_queueCancellationTokenSource;
        private readonly SemaphoreSlim m_syncLock = new SemaphoreSlim(1);
        private readonly WavFileSettings m_wavFileSettings;
        private long m_riffChunkSizePosition;
        private long m_dataChunkSizePosition;

        public bool IsInitialized { get; private set; }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="wavFileSettings"></param>
        public WavFileWriter(string fileName, WavFileSettings wavFileSettings)
        {
           m_fileStream = new FileStream(fileName, FileMode.OpenOrCreate,
                    FileAccess.ReadWrite,
                    FileShare.None);
            m_queueCancellationTokenSource = new CancellationTokenSource();
            m_queueBlock = new BufferBlock<byte[]>(new DataflowBlockOptions { CancellationToken = m_queueCancellationTokenSource.Token});
            m_wavFileSettings = wavFileSettings;
        }

        private void WriteRiffChunk(BinaryWriter bw)
        {
            bw.Write(0x46464952);   //'RIFF'
            //We remember the riffChunkSizePoistion that we have to update later on stream close
            m_riffChunkSizePosition = bw.BaseStream.Position;   //Filelength - 8 bytes
            bw.Write((int)50);  //a 0sec wav file is atleast 58 bytes
            bw.Write(0x45564157);   //'WAVE'
        }

        private void WriteFmtChunk(BinaryWriter bw)
        {
            bw.Write((int)0x20746D66);  //'fmt '
            bw.Write(16);       //16 bytes of format. We produce no 'extra format info'
            bw.Write(m_wavFileSettings.CompressionCode);        //2bytes
            bw.Write(m_wavFileSettings.NumberOfChannels);   //2bytes
            bw.Write(m_wavFileSettings.SampleRate);         //4bytes
            bw.Write(m_wavFileSettings.AvgBytesPerSecond);  //4bytes
            bw.Write((short)2);                 //alignment
            bw.Write((short)16);                //significant bits per sample
        }

        private void WriteFactChunk(BinaryWriter bw)
        {
            bw.Write((int)0x74636166);      //'fact' chunk ID
            bw.Write((int)4);           //4 byte Fact Chunk size
            bw.Write((int)0);           //4 byte chunk data. 
        }

        private void WriteDataChunk(BinaryWriter bw)
        {
            bw.Write(0x61746164);       //'data' chunk ID
            //We remember the dataChunkPosition that we have to update later on stream close
            m_dataChunkSizePosition = bw.BaseStream.Position;
            bw.Write((int)0);               //initially, we have no data, so we set the chunk size to 0
        }

        /// <summary>
        /// Initializes the consumer of the queue to wait for new items and process them if available
        /// </summary>
        /// <returns></returns>
        public async Task InitializeAsync()
        {
            if (!IsInitialized)
            {
                await m_syncLock.WaitAsync();
                if (!IsInitialized)
                {
                    IsInitialized = true;

                    //Initialize the headers
                    Debug.Assert(m_fileStream != null, "m_fileStream != null");
                    BinaryWriter bw = new BinaryWriter(m_fileStream);
                    WriteRiffChunk(bw);
                    WriteFmtChunk(bw);
                    WriteFactChunk(bw);
                    WriteDataChunk(bw);

                    await Task.Factory.StartNew( () =>  DequeueAndProcessAsync());
                }

                m_syncLock.Release();
            }
        }

        /// <summary>
        /// Dequeue and process async workitems
        /// </summary>
        /// <returns></returns>
        internal async Task DequeueAndProcessAsync()
        {
            try
            {
                while (await m_queueBlock.OutputAvailableAsync(m_queueCancellationTokenSource.Token))
                {
                     var buffer = await m_queueBlock.ReceiveAsync(m_queueCancellationTokenSource.Token);

                    if (buffer != null)
                    {
                        await m_fileStream.WriteAsync(buffer, 0, buffer.Length);
                    }

                    this.m_queueCancellationTokenSource.Token.ThrowIfCancellationRequested();
                }
            }
            catch (TaskCanceledException ex)
            {
                Debug.Write(string.Format("The queue processing task has been cancelled. Exception: {0}", ex));
            }
            catch (ObjectDisposedException ex)
            {
                Debug.Write(string.Format("The queue processing task object has been disposed. Exception: {0}", ex));
            }
            catch (Exception ex)
            {
                // Catch all other exceptions and log
                Debug.Write(string.Format("Caught Exception: {0}", ex));

                // Continue processing elements in the queue
                await DequeueAndProcessAsync();
            }
        }

        /// <summary>
        /// Enqueue a waitable work item
        /// </summary>
        /// <param name="buffer"></param>
        /// <returns></returns>
        public async Task EnqueueAsync(byte[] buffer)
        {
            try
            {
                await m_queueBlock.SendAsync(buffer, m_queueCancellationTokenSource.Token);
            }
            catch (TaskCanceledException e)
            {
                if (m_queueBlock != null)
                {
                    m_queueBlock.Complete();
                }

                Debug.Write(string.Format("Cannot enqueue because queuing operation has been cancelled. Exception: {0}", e));
            }
            catch (Exception e)
            {
                Debug.Write(string.Format("Failed to enqueue: {0}", e));
            }
        }

        /// <summary>
        /// ShutDown the queue, this will also cancel current operations
        /// </summary>
        /// <returns></returns>
        public async Task ShutdownAsync()
        {
            if (IsInitialized)
            {
                await m_syncLock.WaitAsync();
                if (IsInitialized)
                {
                    Debug.Assert(m_queueBlock != null);
                    // Allow no more processing on the queue
                    m_queueBlock.Complete();

                    // Cancel the queue task
                    m_queueCancellationTokenSource.Cancel();
                    m_queueCancellationTokenSource.Dispose();
                    IsInitialized = false;
                    CloseFileStream();
                }

                m_syncLock.Release();
            }
        }

        private void CloseFileStream()
        {
            if (m_fileStream != null)
            {

                try
                {
                    using(BinaryWriter bw = new BinaryWriter(m_fileStream))
                    {
                       //Lets update the riffChunkSize header value
                       m_fileStream.Seek(m_riffChunkSizePosition, SeekOrigin.Begin);
                       bw.Write((int)(m_fileStream.Length - 8));

                       //... and the dataChunksize header value;
                       m_fileStream.Seek(m_dataChunkSizePosition, SeekOrigin.Begin);
                       bw.Write((int)(m_fileStream.Length - (m_dataChunkSizePosition + 4)));
                    }
                }
                finally
                {
                    m_fileStream.Close();
                }
            }
        }
    }

    internal class WavFileSettings
    {
        public short CompressionCode { get; set; }
        public short NumberOfChannels { get; set; }
        public int SampleRate { get; set; }
        public int AvgBytesPerSecond { get; set; }

        /// <summary>
        /// Default constructor with default PCM 16 mono
        /// </summary>
        public WavFileSettings()
        {
            CompressionCode = 1;            //PCM
            NumberOfChannels = 1;           //No Stereo
            SampleRate = 16000;             //16khz only
            AvgBytesPerSecond = 32000;
        }
    }
ssulzer commented 7 years ago

Are you trying to record within a multiparty group call? Recording the audio of individual participants in a group call is not supported. Please be aware that bots for group calls are currently not supported and such bots will likely stop working soon.

gfclaveria commented 7 years ago

@waboum Thanks! I will try this out.

@ssulzer Yes, I am hoping i could record multiparty group call. I currently working with the MeetingScreenshotsBot and it seems that bot is working as it should with group call. From what you've said, does that mean that this sample bot you have (or the feature it has) will be ditched out soon? May I know if this will be supported in the future. Also, bots for Skype For Business??

Thanks,

ssulzer commented 7 years ago

@gfclaveria Upcoming changes will prevent calling and real-time media bots from joining Skype group calls, and unfortunately no timeframe regarding when they will be supported. Calling and real-time media bots will be allowed only in 1-to-1 Skype calls.

No information yet regarding calling and media bots for Skype for Business Online are not supported.

gfclaveria commented 7 years ago

@ssulzer Additional question. May we known the reason why will you be removing group calls feature for bot?

MalarGit commented 7 years ago

@gfclaveria currently the bot can record audio and video of any user in the conference which is a privacy concern.