Home | History | Annotate | Download | only in webrtcdemo
      1 /*
      2  *  Copyright (c) 2013 The WebRTC project authors. All Rights Reserved.
      3  *
      4  *  Use of this source code is governed by a BSD-style license
      5  *  that can be found in the LICENSE file in the root of the source
      6  *  tree. An additional intellectual property rights grant can be found
      7  *  in the file PATENTS.  All contributing project authors may
      8  *  be found in the AUTHORS file in the root of the source tree.
      9  */
     10 
     11 package org.webrtc.webrtcdemo;
     12 
     13 import org.webrtc.videoengine.ViERenderer;
     14 import org.webrtc.videoengine.VideoCaptureAndroid;
     15 
     16 import android.app.AlertDialog;
     17 import android.content.BroadcastReceiver;
     18 import android.content.Context;
     19 import android.content.DialogInterface;
     20 import android.content.Intent;
     21 import android.content.IntentFilter;
     22 import android.hardware.Camera.CameraInfo;
     23 import android.hardware.Camera;
     24 import android.hardware.SensorManager;
     25 import android.media.AudioManager;
     26 import android.os.Environment;
     27 import android.util.Log;
     28 import android.view.OrientationEventListener;
     29 import android.view.SurfaceView;
     30 import java.io.File;
     31 
     32 public class MediaEngine implements VideoDecodeEncodeObserver {
     33   // TODO(henrike): Most of these should be moved to xml (since static).
     34   private static final int VCM_VP8_PAYLOAD_TYPE = 100;
     35   private static final int SEND_CODEC_FPS = 30;
     36   // TODO(henrike): increase INIT_BITRATE_KBPS to 2000 and ensure that
     37   // 720p30fps can be acheived (on hardware that can handle it). Note that
     38   // setting 2000 currently leads to failure, so that has to be resolved first.
     39   private static final int INIT_BITRATE_KBPS = 500;
     40   private static final int MAX_BITRATE_KBPS = 3000;
     41   private static final String LOG_DIR = "webrtc";
     42   private static final int WIDTH_IDX = 0;
     43   private static final int HEIGHT_IDX = 1;
     44   private static final int[][] RESOLUTIONS = {
     45     {176,144}, {320,240}, {352,288}, {640,480}, {1280,720}
     46   };
     47   // Arbitrary choice of 4/5 volume (204/256).
     48   private static final int volumeLevel = 204;
     49 
     50   public static int numberOfResolutions() { return RESOLUTIONS.length; }
     51 
     52   public static String[] resolutionsAsString() {
     53     String[] retVal = new String[numberOfResolutions()];
     54     for (int i = 0; i < numberOfResolutions(); ++i) {
     55       retVal[i] = RESOLUTIONS[i][0] + "x" + RESOLUTIONS[i][1];
     56     }
     57     return retVal;
     58   }
     59 
     60   // Checks for and communicate failures to user (logcat and popup).
     61   private void check(boolean value, String message) {
     62     if (value) {
     63       return;
     64     }
     65     Log.e("WEBRTC-CHECK", message);
     66     AlertDialog alertDialog = new AlertDialog.Builder(context).create();
     67     alertDialog.setTitle("WebRTC Error");
     68     alertDialog.setMessage(message);
     69     alertDialog.setButton(DialogInterface.BUTTON_POSITIVE,
     70         "OK",
     71         new DialogInterface.OnClickListener() {
     72           public void onClick(DialogInterface dialog, int which) {
     73             dialog.dismiss();
     74             return;
     75           }
     76         }
     77                           );
     78     alertDialog.show();
     79   }
     80 
     81   // Converts device rotation to camera rotation. Rotation depends on if the
     82   // camera is back facing and rotate with the device or front facing and
     83   // rotating in the opposite direction of the device.
     84   private static int rotationFromRealWorldUp(CameraInfo info,
     85                                              int deviceRotation) {
     86     int coarseDeviceOrientation =
     87         (int)(Math.round((double)deviceRotation / 90) * 90) % 360;
     88     if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
     89       // The front camera rotates in the opposite direction of the
     90       // device.
     91       int inverseDeviceOrientation = 360 - coarseDeviceOrientation;
     92       return (inverseDeviceOrientation + info.orientation) % 360;
     93     }
     94     return (coarseDeviceOrientation + info.orientation) % 360;
     95   }
     96 
     97   // Shared Audio/Video members.
     98   private final Context context;
     99   private String remoteIp;
    100   private boolean enableTrace;
    101 
    102     // Audio
    103   private VoiceEngine voe;
    104   private int audioChannel;
    105   private boolean audioEnabled;
    106   private boolean voeRunning;
    107   private int audioCodecIndex;
    108   private int audioTxPort;
    109   private int audioRxPort;
    110 
    111   private boolean speakerEnabled;
    112   private boolean headsetPluggedIn;
    113   private boolean enableAgc;
    114   private boolean enableNs;
    115   private boolean enableAecm;
    116 
    117   private BroadcastReceiver headsetListener;
    118 
    119   private boolean audioRtpDump;
    120   private boolean apmRecord;
    121 
    122   // Video
    123   private VideoEngine vie;
    124   private int videoChannel;
    125   private boolean receiveVideo;
    126   private boolean sendVideo;
    127   private boolean vieRunning;
    128   private int videoCodecIndex;
    129   private int resolutionIndex;
    130   private int videoTxPort;
    131   private int videoRxPort;
    132 
    133   // Indexed by CameraInfo.CAMERA_FACING_{BACK,FRONT}.
    134   private CameraInfo cameras[];
    135   private boolean useFrontCamera;
    136   private int currentCameraHandle;
    137   private boolean enableNack;
    138   // openGl, surfaceView or mediaCodec (integers.xml)
    139   private int viewSelection;
    140   private boolean videoRtpDump;
    141 
    142   private SurfaceView svLocal;
    143   private SurfaceView svRemote;
    144   MediaCodecVideoDecoder externalCodec;
    145 
    146   private int inFps;
    147   private int inKbps;
    148   private int outFps;
    149   private int outKbps;
    150   private int inWidth;
    151   private int inHeight;
    152 
    153   private OrientationEventListener orientationListener;
    154   private int deviceOrientation = OrientationEventListener.ORIENTATION_UNKNOWN;
    155 
    156   public MediaEngine(Context context) {
    157     this.context = context;
    158     voe = new VoiceEngine();
    159     check(voe.init() == 0, "Failed voe Init");
    160     audioChannel = voe.createChannel();
    161     check(audioChannel >= 0, "Failed voe CreateChannel");
    162     vie = new VideoEngine();
    163     check(vie.init() == 0, "Failed voe Init");
    164     check(vie.setVoiceEngine(voe) == 0, "Failed setVoiceEngine");
    165     videoChannel = vie.createChannel();
    166     check(audioChannel >= 0, "Failed voe CreateChannel");
    167     check(vie.connectAudioChannel(videoChannel, audioChannel) == 0,
    168         "Failed ConnectAudioChannel");
    169 
    170     cameras = new CameraInfo[2];
    171     CameraInfo info = new CameraInfo();
    172     for (int i = 0; i < Camera.getNumberOfCameras(); ++i) {
    173       Camera.getCameraInfo(i, info);
    174       cameras[info.facing] = info;
    175     }
    176     setDefaultCamera();
    177     check(voe.setSpeakerVolume(volumeLevel) == 0,
    178         "Failed setSpeakerVolume");
    179     check(voe.setAecmMode(VoiceEngine.AecmModes.SPEAKERPHONE, false) == 0,
    180         "VoE set Aecm speakerphone mode failed");
    181     check(vie.setKeyFrameRequestMethod(videoChannel,
    182             VideoEngine.VieKeyFrameRequestMethod.
    183             KEY_FRAME_REQUEST_PLI_RTCP) == 0,
    184         "Failed setKeyFrameRequestMethod");
    185     check(vie.registerObserver(videoChannel, this) == 0,
    186         "Failed registerObserver");
    187 
    188     // TODO(hellner): SENSOR_DELAY_NORMAL?
    189     // Listen to changes in device orientation.
    190     orientationListener =
    191         new OrientationEventListener(context, SensorManager.SENSOR_DELAY_UI) {
    192           public void onOrientationChanged (int orientation) {
    193             deviceOrientation = orientation;
    194             compensateRotation();
    195           }
    196         };
    197     orientationListener.enable();
    198     // Set audio mode to communication
    199     AudioManager audioManager =
    200         ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE));
    201     audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
    202     // Listen to headset being plugged in/out.
    203     IntentFilter receiverFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
    204     headsetListener = new BroadcastReceiver() {
    205         @Override
    206         public void onReceive(Context context, Intent intent) {
    207           if (intent.getAction().compareTo(Intent.ACTION_HEADSET_PLUG) == 0) {
    208             headsetPluggedIn = intent.getIntExtra("state", 0) == 1;
    209             updateAudioOutput();
    210           }
    211         }
    212       };
    213     context.registerReceiver(headsetListener, receiverFilter);
    214   }
    215 
    216   public void dispose() {
    217     check(!voeRunning && !voeRunning, "Engines must be stopped before dispose");
    218     context.unregisterReceiver(headsetListener);
    219     orientationListener.disable();
    220     check(vie.deregisterObserver(videoChannel) == 0,
    221         "Failed deregisterObserver");
    222     if (externalCodec != null) {
    223       check(vie.deRegisterExternalReceiveCodec(videoChannel,
    224               VCM_VP8_PAYLOAD_TYPE) == 0,
    225           "Failed to deregister external decoder");
    226       externalCodec = null;
    227     }
    228     check(vie.deleteChannel(videoChannel) == 0, "DeleteChannel");
    229     vie.dispose();
    230     check(voe.deleteChannel(audioChannel) == 0, "VoE delete channel failed");
    231     voe.dispose();
    232   }
    233 
    234   public void start() {
    235     if (audioEnabled) {
    236       startVoE();
    237     }
    238     if (receiveVideo || sendVideo) {
    239       startViE();
    240     }
    241   }
    242 
    243   public void stop() {
    244     stopVoe();
    245     stopVie();
    246   }
    247 
    248   public boolean isRunning() {
    249     return voeRunning || vieRunning;
    250   }
    251 
    252   public void setRemoteIp(String remoteIp) {
    253     this.remoteIp = remoteIp;
    254     UpdateSendDestination();
    255   }
    256 
    257   public String remoteIp() { return remoteIp; }
    258 
    259   public void setTrace(boolean enable) {
    260     if (enable) {
    261       vie.setTraceFile("/sdcard/trace.txt", false);
    262       vie.setTraceFilter(VideoEngine.TraceLevel.TRACE_ALL);
    263       return;
    264     }
    265     vie.setTraceFilter(VideoEngine.TraceLevel.TRACE_NONE);
    266   }
    267 
    268   private String getDebugDirectory() {
    269     // Should create a folder in /scard/|LOG_DIR|
    270     return Environment.getExternalStorageDirectory().toString() + "/" +
    271         LOG_DIR;
    272   }
    273 
    274   private boolean createDebugDirectory() {
    275     File webrtc_dir = new File(getDebugDirectory());
    276     if (!webrtc_dir.exists()) {
    277       return webrtc_dir.mkdir();
    278     }
    279     return webrtc_dir.isDirectory();
    280   }
    281 
    282   public void startVoE() {
    283     check(!voeRunning, "VoE already started");
    284     check(voe.startListen(audioChannel) == 0, "Failed StartListen");
    285     check(voe.startPlayout(audioChannel) == 0, "VoE start playout failed");
    286     check(voe.startSend(audioChannel) == 0, "VoE start send failed");
    287     voeRunning = true;
    288   }
    289 
    290   private void stopVoe() {
    291     check(voeRunning, "VoE not started");
    292     check(voe.stopSend(audioChannel) == 0, "VoE stop send failed");
    293     check(voe.stopPlayout(audioChannel) == 0, "VoE stop playout failed");
    294     check(voe.stopListen(audioChannel) == 0, "VoE stop listen failed");
    295     voeRunning = false;
    296   }
    297 
    298   public void setAudio(boolean audioEnabled) {
    299     this.audioEnabled = audioEnabled;
    300   }
    301 
    302   public boolean audioEnabled() { return audioEnabled; }
    303 
    304   public int audioCodecIndex() { return audioCodecIndex; }
    305 
    306   public void setAudioCodec(int codecNumber) {
    307     audioCodecIndex = codecNumber;
    308     CodecInst codec = voe.getCodec(codecNumber);
    309     check(voe.setSendCodec(audioChannel, codec) == 0, "Failed setSendCodec");
    310     codec.dispose();
    311   }
    312 
    313   public String[] audioCodecsAsString() {
    314     String[] retVal = new String[voe.numOfCodecs()];
    315     for (int i = 0; i < voe.numOfCodecs(); ++i) {
    316       CodecInst codec = voe.getCodec(i);
    317       retVal[i] = codec.toString();
    318       codec.dispose();
    319     }
    320     return retVal;
    321   }
    322 
    323   private CodecInst[] defaultAudioCodecs() {
    324     CodecInst[] retVal = new CodecInst[voe.numOfCodecs()];
    325      for (int i = 0; i < voe.numOfCodecs(); ++i) {
    326       retVal[i] = voe.getCodec(i);
    327     }
    328     return retVal;
    329   }
    330 
    331   public int getIsacIndex() {
    332     CodecInst[] codecs = defaultAudioCodecs();
    333     for (int i = 0; i < codecs.length; ++i) {
    334       if (codecs[i].name().contains("ISAC")) {
    335         return i;
    336       }
    337     }
    338     return 0;
    339   }
    340 
    341   public void setAudioTxPort(int audioTxPort) {
    342     this.audioTxPort = audioTxPort;
    343     UpdateSendDestination();
    344   }
    345 
    346   public int audioTxPort() { return audioTxPort; }
    347 
    348   public void setAudioRxPort(int audioRxPort) {
    349     check(voe.setLocalReceiver(audioChannel, audioRxPort) == 0,
    350         "Failed setLocalReceiver");
    351     this.audioRxPort = audioRxPort;
    352   }
    353 
    354   public int audioRxPort() { return audioRxPort; }
    355 
    356   public boolean agcEnabled() { return enableAgc; }
    357 
    358   public void setAgc(boolean enable) {
    359     enableAgc = enable;
    360     VoiceEngine.AgcConfig agc_config =
    361         new VoiceEngine.AgcConfig(3, 9, true);
    362     check(voe.setAgcConfig(agc_config) == 0, "VoE set AGC Config failed");
    363     check(voe.setAgcStatus(enableAgc, VoiceEngine.AgcModes.FIXED_DIGITAL) == 0,
    364         "VoE set AGC Status failed");
    365   }
    366 
    367   public boolean nsEnabled() { return enableNs; }
    368 
    369   public void setNs(boolean enable) {
    370     enableNs = enable;
    371     check(voe.setNsStatus(enableNs,
    372             VoiceEngine.NsModes.MODERATE_SUPPRESSION) == 0,
    373         "VoE set NS Status failed");
    374   }
    375 
    376   public boolean aecmEnabled() { return enableAecm; }
    377 
    378   public void setEc(boolean enable) {
    379     enableAecm = enable;
    380     check(voe.setEcStatus(enable, VoiceEngine.EcModes.AECM) == 0,
    381         "voe setEcStatus");
    382   }
    383 
    384   public boolean speakerEnabled() {
    385     return speakerEnabled;
    386   }
    387 
    388   public void setSpeaker(boolean enable) {
    389     speakerEnabled = enable;
    390     updateAudioOutput();
    391   }
    392 
    393   // Debug helpers.
    394   public boolean apmRecord() { return apmRecord; }
    395 
    396   public boolean audioRtpDump() { return audioRtpDump; }
    397 
    398   public void setDebuging(boolean enable) {
    399     apmRecord = enable;
    400     if (!enable) {
    401       check(voe.stopDebugRecording() == 0, "Failed stopping debug");
    402       return;
    403     }
    404     if (!createDebugDirectory()) {
    405       check(false, "Unable to create debug directory.");
    406       return;
    407     }
    408     String debugDirectory = getDebugDirectory();
    409     check(voe.startDebugRecording(debugDirectory +  String.format("/apm_%d.dat",
    410                 System.currentTimeMillis())) == 0,
    411         "Failed starting debug");
    412   }
    413 
    414   public void setIncomingVoeRtpDump(boolean enable) {
    415     audioRtpDump = enable;
    416     if (!enable) {
    417       check(voe.stopRtpDump(videoChannel,
    418               VoiceEngine.RtpDirections.INCOMING) == 0,
    419           "voe stopping rtp dump");
    420       return;
    421     }
    422     String debugDirectory = getDebugDirectory();
    423     check(voe.startRtpDump(videoChannel, debugDirectory +
    424             String.format("/voe_%d.rtp", System.currentTimeMillis()),
    425             VoiceEngine.RtpDirections.INCOMING) == 0,
    426         "voe starting rtp dump");
    427   }
    428 
    429   private void updateAudioOutput() {
    430     boolean useSpeaker = !headsetPluggedIn && speakerEnabled;
    431     AudioManager audioManager =
    432         ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE));
    433     audioManager.setSpeakerphoneOn(useSpeaker);
    434   }
    435 
    436   public void startViE() {
    437     check(!vieRunning, "ViE already started");
    438 
    439     if (receiveVideo) {
    440       if (viewSelection ==
    441           context.getResources().getInteger(R.integer.openGl)) {
    442         svRemote = ViERenderer.CreateRenderer(context, true);
    443       } else if (viewSelection ==
    444           context.getResources().getInteger(R.integer.surfaceView)) {
    445         svRemote = ViERenderer.CreateRenderer(context, false);
    446       } else {
    447         externalCodec = new MediaCodecVideoDecoder(context);
    448         svRemote = externalCodec.getView();
    449       }
    450       if (externalCodec != null) {
    451         check(vie.registerExternalReceiveCodec(videoChannel,
    452                 VCM_VP8_PAYLOAD_TYPE, externalCodec, true) == 0,
    453             "Failed to register external decoder");
    454       } else {
    455         check(vie.addRenderer(videoChannel, svRemote,
    456                 0, 0, 0, 1, 1) == 0, "Failed AddRenderer");
    457         check(vie.startRender(videoChannel) == 0, "Failed StartRender");
    458       }
    459       check(vie.startReceive(videoChannel) == 0, "Failed StartReceive");
    460     }
    461     if (sendVideo) {
    462       startCamera();
    463       check(vie.startSend(videoChannel) == 0, "Failed StartSend");
    464     }
    465     vieRunning = true;
    466   }
    467 
    468   private void stopVie() {
    469     if (!vieRunning) {
    470       return;
    471     }
    472     check(vie.stopSend(videoChannel) == 0, "StopSend");
    473     stopCamera();
    474     check(vie.stopReceive(videoChannel) == 0, "StopReceive");
    475     if (externalCodec != null) {
    476       check(vie.deRegisterExternalReceiveCodec(videoChannel,
    477               VCM_VP8_PAYLOAD_TYPE) == 0,
    478               "Failed to deregister external decoder");
    479       externalCodec.dispose();
    480       externalCodec = null;
    481     } else {
    482       check(vie.stopRender(videoChannel) == 0, "StopRender");
    483       check(vie.removeRenderer(videoChannel) == 0, "RemoveRenderer");
    484     }
    485     svRemote = null;
    486     vieRunning = false;
    487   }
    488 
    489   public void setReceiveVideo(boolean receiveVideo) {
    490     this.receiveVideo = receiveVideo;
    491   }
    492 
    493   public boolean receiveVideo() { return receiveVideo; }
    494 
    495   public void setSendVideo(boolean sendVideo) { this.sendVideo = sendVideo; }
    496 
    497   public boolean sendVideo() { return sendVideo; }
    498 
    499   public int videoCodecIndex() { return videoCodecIndex; }
    500 
    501   public void setVideoCodec(int codecNumber) {
    502     videoCodecIndex = codecNumber;
    503     updateVideoCodec();
    504   }
    505 
    506   public String[] videoCodecsAsString() {
    507     String[] retVal = new String[vie.numberOfCodecs()];
    508     for (int i = 0; i < vie.numberOfCodecs(); ++i) {
    509       VideoCodecInst codec = vie.getCodec(i);
    510       retVal[i] = codec.toString();
    511       codec.dispose();
    512     }
    513     return retVal;
    514   }
    515 
    516   public int resolutionIndex() { return resolutionIndex; }
    517 
    518   public void setResolutionIndex(int resolution) {
    519     resolutionIndex = resolution;
    520     updateVideoCodec();
    521   }
    522 
    523   private void updateVideoCodec() {
    524     VideoCodecInst codec = getVideoCodec(videoCodecIndex, resolutionIndex);
    525     check(vie.setSendCodec(videoChannel, codec) == 0, "Failed setReceiveCodec");
    526     codec.dispose();
    527   }
    528 
    529   private VideoCodecInst getVideoCodec(int codecNumber, int resolution) {
    530     VideoCodecInst retVal = vie.getCodec(codecNumber);
    531     retVal.setStartBitRate(INIT_BITRATE_KBPS);
    532     retVal.setMaxBitRate(MAX_BITRATE_KBPS);
    533     retVal.setWidth(RESOLUTIONS[resolution][WIDTH_IDX]);
    534     retVal.setHeight(RESOLUTIONS[resolution][HEIGHT_IDX]);
    535     retVal.setMaxFrameRate(SEND_CODEC_FPS);
    536     return retVal;
    537   }
    538 
    539   public void setVideoRxPort(int videoRxPort) {
    540     this.videoRxPort = videoRxPort;
    541     check(vie.setLocalReceiver(videoChannel, videoRxPort) == 0,
    542         "Failed setLocalReceiver");
    543   }
    544 
    545   public int videoRxPort() { return videoRxPort; }
    546 
    547   public void setVideoTxPort(int videoTxPort) {
    548     this.videoTxPort = videoTxPort;
    549     UpdateSendDestination();
    550   }
    551 
    552   private void UpdateSendDestination() {
    553     if (remoteIp == null) {
    554       return;
    555     }
    556     if (audioTxPort != 0) {
    557       check(voe.setSendDestination(audioChannel, audioTxPort,
    558               remoteIp) == 0, "VoE set send destination failed");
    559     }
    560     if (videoTxPort != 0) {
    561       check(vie.setSendDestination(videoChannel, videoTxPort, remoteIp) == 0,
    562           "Failed setSendDestination");
    563     }
    564 
    565     // Setting localSSRC manually (arbitrary value) for loopback test,
    566     // As otherwise we will get a clash and a new SSRC will be set,
    567     // Which will reset the receiver and other minor issues.
    568     if (remoteIp.equals("127.0.0.1")) {
    569       check(vie.setLocalSSRC(videoChannel, 0x01234567) == 0,
    570            "Failed setLocalSSRC");
    571     }
    572   }
    573 
    574   public int videoTxPort() {
    575     return videoTxPort;
    576   }
    577 
    578   public boolean hasMultipleCameras() {
    579     return Camera.getNumberOfCameras() > 1;
    580   }
    581 
    582   public boolean frontCameraIsSet() {
    583     return useFrontCamera;
    584   }
    585 
    586   // Set default camera to front if there is a front camera.
    587   private void setDefaultCamera() {
    588     useFrontCamera = hasFrontCamera();
    589   }
    590 
    591   public void toggleCamera() {
    592     if (vieRunning) {
    593       stopCamera();
    594     }
    595     useFrontCamera = !useFrontCamera;
    596     if (vieRunning) {
    597       startCamera();
    598     }
    599   }
    600 
    601   private void startCamera() {
    602     CameraDesc cameraInfo = vie.getCaptureDevice(getCameraId(getCameraIndex()));
    603     currentCameraHandle = vie.allocateCaptureDevice(cameraInfo);
    604     cameraInfo.dispose();
    605     check(vie.connectCaptureDevice(currentCameraHandle, videoChannel) == 0,
    606         "Failed to connect capture device");
    607     // Camera and preview surface.
    608     svLocal = new SurfaceView(context);
    609     VideoCaptureAndroid.setLocalPreview(svLocal.getHolder());
    610     check(vie.startCapture(currentCameraHandle) == 0, "Failed StartCapture");
    611     compensateRotation();
    612   }
    613 
    614   private void stopCamera() {
    615     check(vie.stopCapture(currentCameraHandle) == 0, "Failed StopCapture");
    616     svLocal = null;
    617     check(vie.releaseCaptureDevice(currentCameraHandle) == 0,
    618         "Failed ReleaseCaptureDevice");
    619   }
    620 
    621   private boolean hasFrontCamera() {
    622     return cameras[CameraInfo.CAMERA_FACING_FRONT] != null;
    623   }
    624 
    625   public SurfaceView getRemoteSurfaceView() {
    626     return svRemote;
    627   }
    628 
    629   public SurfaceView getLocalSurfaceView() {
    630     return svLocal;
    631   }
    632 
    633   public void setViewSelection(int viewSelection) {
    634     this.viewSelection = viewSelection;
    635   }
    636 
    637   public int viewSelection() { return viewSelection; }
    638 
    639   public boolean nackEnabled() { return enableNack; }
    640 
    641   public void setNack(boolean enable) {
    642     enableNack = enable;
    643     check(vie.setNackStatus(videoChannel, enableNack) == 0,
    644         "Failed setNackStatus");
    645   }
    646 
    647   // Collates current state into a multiline string.
    648   public String sendReceiveState() {
    649     int packetLoss = 0;
    650     if (vieRunning) {
    651       RtcpStatistics stats = vie.getReceivedRtcpStatistics(videoChannel);
    652       if (stats != null) {
    653         // Calculate % lost from fraction lost.
    654         // Definition of fraction lost can be found in RFC3550.
    655         packetLoss = (stats.fractionLost * 100) >> 8;
    656       }
    657     }
    658     String retVal =
    659         "fps in/out: " + inFps + "/" + outFps + "\n" +
    660         "kBps in/out: " + inKbps / 1024 + "/ " + outKbps / 1024 + "\n" +
    661         "resolution: " + inWidth + "x" + inHeight + "\n" +
    662         "loss: " + packetLoss + "%";
    663     return retVal;
    664   }
    665 
    666   MediaEngineObserver observer;
    667   public void setObserver(MediaEngineObserver observer) {
    668     this.observer = observer;
    669   }
    670 
    671   // Callbacks from the VideoDecodeEncodeObserver interface.
    672   public void incomingRate(int videoChannel, int framerate, int bitrate) {
    673     inFps = framerate;
    674     inKbps = bitrate;
    675     newStats();
    676   }
    677 
    678   public void incomingCodecChanged(int videoChannel,
    679       VideoCodecInst videoCodec) {
    680     inWidth = videoCodec.width();
    681     inHeight = videoCodec.height();
    682     videoCodec.dispose();
    683     newStats();
    684   }
    685 
    686   public void requestNewKeyFrame(int videoChannel) {}
    687 
    688   public void outgoingRate(int videoChannel, int framerate, int bitrate) {
    689     outFps = framerate;
    690     outKbps = bitrate;
    691     newStats();
    692   }
    693 
    694   private void newStats() {
    695     if (observer != null) {
    696       observer.newStats(sendReceiveState());
    697     }
    698   }
    699 
    700   // Debug helpers.
    701   public boolean videoRtpDump() { return videoRtpDump; }
    702 
    703   public void setIncomingVieRtpDump(boolean enable) {
    704     videoRtpDump = enable;
    705     if (!enable) {
    706       check(vie.stopRtpDump(videoChannel,
    707               VideoEngine.RtpDirections.INCOMING) == 0,
    708           "vie StopRTPDump");
    709       return;
    710     }
    711     String debugDirectory = getDebugDirectory();
    712     check(vie.startRtpDump(videoChannel, debugDirectory +
    713             String.format("/vie_%d.rtp", System.currentTimeMillis()),
    714             VideoEngine.RtpDirections.INCOMING) == 0,
    715         "vie StartRtpDump");
    716   }
    717 
    718   private int getCameraIndex() {
    719     return useFrontCamera ? Camera.CameraInfo.CAMERA_FACING_FRONT :
    720         Camera.CameraInfo.CAMERA_FACING_BACK;
    721   }
    722 
    723   private int getCameraId(int index) {
    724     for (int i = Camera.getNumberOfCameras() - 1; i >= 0; --i) {
    725       CameraInfo info = new CameraInfo();
    726       Camera.getCameraInfo(i, info);
    727       if (index == info.facing) {
    728         return i;
    729       }
    730     }
    731     throw new RuntimeException("Index does not match a camera");
    732   }
    733 
    734   private void compensateRotation() {
    735     if (svLocal == null) {
    736       // Not rendering (or sending).
    737       return;
    738     }
    739     if (deviceOrientation == OrientationEventListener.ORIENTATION_UNKNOWN) {
    740       return;
    741     }
    742     int cameraRotation = rotationFromRealWorldUp(
    743         cameras[getCameraIndex()], deviceOrientation);
    744     // Egress streams should have real world up as up.
    745     check(
    746         vie.setRotateCapturedFrames(currentCameraHandle, cameraRotation) == 0,
    747         "Failed setRotateCapturedFrames: camera " + currentCameraHandle +
    748         "rotation " + cameraRotation);
    749   }
    750 }
    751