/** * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ * * Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below). * * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software * Foundation; either version 3.0 of the License, or (at your option) any later * version. * * BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License along * with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. * */ package org.bigbluebutton.voiceconf.red5.media.transcoder; import java.io.IOException; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.nio.FloatBuffer; import java.util.Random; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import org.slf4j.Logger; import org.bigbluebutton.voiceconf.red5.media.SipToFlashAudioStream; import org.red5.logging.Red5LoggerFactory; import org.red5.server.api.IConnection; import org.red5.server.api.Red5; import org.red5.server.net.rtmp.RTMPMinaConnection; import org.red5.app.sip.codecs.Codec; import org.red5.app.sip.codecs.asao.CodecImpl; public class NellySipToFlashTranscoderImp implements SipToFlashTranscoder { protected static Logger log = Red5LoggerFactory.getLogger(NellySipToFlashTranscoderImp.class, "sip"); private static final int NELLYMOSER_CODEC_ID = 82; /** * The length of resulting L16 audio converted from 160-byte Ulaw audio. */ private static final int L16_AUDIO_LENGTH = 256; /** * The length of Nelly audio that gets sent to Flash Player. */ private static final int NELLY_AUDIO_LENGTH = 64; /** * The length of received Ulaw audio. */ private static final int ULAW_AUDIO_LENGTH = 160; /** * The maximum size of our processing buffer. 8 Ulaw packets (8x160 = 1280) yields 5 L16/Nelly audio (5x256 = 1280). */ private static final int MAX_BUFFER_LENGTH = 1280; /** * Buffer that contain L16 transcoded audio from Ulaw. */ private final FloatBuffer l16Audio = FloatBuffer.allocate(MAX_BUFFER_LENGTH); /* * A view read-only buffer that keeps track of which part of the L16 buffer will be converted to Nelly. */ private FloatBuffer viewBuffer; private final float[] tempL16Buffer = new float[ULAW_AUDIO_LENGTH]; private float[] tempNellyBuffer = new float[L16_AUDIO_LENGTH]; private final byte[] nellyBytes = new byte[NELLY_AUDIO_LENGTH]; private float[] encoderMap; private Codec audioCodec = null; private long timestamp = 0; private final static int TS_INCREMENT = 32; // Determined from PCAP traces. private final PipedOutputStream streamFromSip; private PipedInputStream streamToFlash; private TranscodedAudioDataListener transcodedAudioListener; private boolean processAudioData; private final Executor exec = Executors.newSingleThreadExecutor(); private Runnable audioDataProcessor; /** * The transcode takes a 160-byte Ulaw audio and converts it to a 160-float L16 audio. Whenever there is an * available 256-float L16 audio, that gets converted into a 64-byte Nelly audio. Therefore, 8 Ulaw packets * are needed to generate 5 Nelly packets. * @param audioCodec */ public NellySipToFlashTranscoderImp(Codec audioCodec) { this.audioCodec = audioCodec; encoderMap = new float[64]; Random rgen = new Random(); timestamp = rgen.nextInt(1000); viewBuffer = l16Audio.asReadOnlyBuffer(); streamFromSip = new PipedOutputStream(); try { streamToFlash = new PipedInputStream(streamFromSip); } catch (IOException e) { e.printStackTrace(); } } @Override public void transcode(byte[] audioData) { if (audioData.length != ULAW_AUDIO_LENGTH) { if (log.isWarnEnabled()) log.warn("Received corrupt audio. Got {}, expected {}.", audioData.length, ULAW_AUDIO_LENGTH); return; } // Convert Ulaw to L16 audioCodec.codecToPcm(audioData, tempL16Buffer); // Store into the buffer l16Audio.put(tempL16Buffer); if ((l16Audio.position() - viewBuffer.position()) >= L16_AUDIO_LENGTH) { // We have enough L16 audio to generate a Nelly audio. // Get some L16 audio viewBuffer.get(tempNellyBuffer); // Convert it into Nelly encoderMap = CodecImpl.encode(encoderMap, tempNellyBuffer, nellyBytes); // Having done all of that, we now see if we need to send the audio or drop it. // We have to encode to build the encoderMap so that data from previous audio packet // will be used for the next packet. boolean sendPacket = true; IConnection conn = Red5.getConnectionLocal(); if (conn instanceof RTMPMinaConnection) { long pendingMessages = ((RTMPMinaConnection)conn).getPendingMessages(); if (pendingMessages > 25) { // Message backed up probably due to slow connection to client (25 messages * 20ms ptime = 500ms audio) sendPacket = false; if (log.isInfoEnabled()) log.info("Dropping packet. Connection {} congested with {} pending messages (~500ms worth of audio) .", conn.getClient().getId(), pendingMessages); } } if (sendPacket) transcodedAudioListener.handleTranscodedAudioData(nellyBytes, timestamp += TS_INCREMENT); } if (l16Audio.position() == l16Audio.capacity()) { // We've processed 8 Ulaw packets (5 Nelly packets), reset the buffers. l16Audio.clear(); viewBuffer.clear(); } } @Override public int getIncomingEncodedFrameSize() { return audioCodec.getIncomingEncodedFrameSize(); } @Override public int getCodecId() { return NELLYMOSER_CODEC_ID; } @Override public void handleData(byte[] audioData, int offset, int len) { try { streamFromSip.write(audioData, offset, len); } catch (IOException e) { e.printStackTrace(); } } private void processAudioData() { int len = 160; byte[] pcmAudio = new byte[len]; int remaining = len; int offset = 0; while (processAudioData) { try { int bytesRead = streamToFlash.read(pcmAudio, offset, remaining); remaining -= bytesRead; if (remaining == 0) { remaining = len; offset = 0; transcode(pcmAudio); } else { offset += bytesRead; } } catch (IOException e) { e.printStackTrace(); } } } @Override public void start(){ processAudioData = true; audioDataProcessor = new Runnable() { public void run() { processAudioData(); } }; exec.execute(audioDataProcessor); } @Override public void stop() { processAudioData = false; } @Override public void setTranscodedAudioListener(SipToFlashAudioStream sipToFlashAudioStream) { this.transcodedAudioListener = sipToFlashAudioStream; } }