2 * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved.
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.
11 package org.webrtc.webrtcdemo;
13 import org.webrtc.videoengine.ViERenderer;
15 import android.app.AlertDialog;
16 import android.content.BroadcastReceiver;
17 import android.content.Context;
18 import android.content.DialogInterface;
19 import android.content.Intent;
20 import android.content.IntentFilter;
21 import android.hardware.Camera;
22 import android.hardware.Camera.CameraInfo;
23 import android.hardware.SensorManager;
24 import android.os.Environment;
25 import android.util.Log;
26 import android.view.OrientationEventListener;
27 import android.view.SurfaceView;
30 public class MediaEngine implements VideoDecodeEncodeObserver {
31 // TODO(henrike): Most of these should be moved to xml (since static).
32 private static final int VCM_VP8_PAYLOAD_TYPE = 100;
33 private static final int SEND_CODEC_FPS = 30;
34 // TODO(henrike): increase INIT_BITRATE_KBPS to 2000 and ensure that
35 // 720p30fps can be acheived (on hardware that can handle it). Note that
36 // setting 2000 currently leads to failure, so that has to be resolved first.
37 private static final int INIT_BITRATE_KBPS = 500;
38 private static final int MAX_BITRATE_KBPS = 3000;
39 private static final String LOG_DIR = "webrtc";
40 private static final int WIDTH_IDX = 0;
41 private static final int HEIGHT_IDX = 1;
42 private static final int[][] RESOLUTIONS = {
43 {176,144}, {320,240}, {352,288}, {640,480}, {1280,720}
45 // Arbitrary choice of 4/5 volume (204/256).
46 private static final int volumeLevel = 204;
48 public static int numberOfResolutions() { return RESOLUTIONS.length; }
50 public static String[] resolutionsAsString() {
51 String[] retVal = new String[numberOfResolutions()];
52 for (int i = 0; i < numberOfResolutions(); ++i) {
53 retVal[i] = RESOLUTIONS[i][0] + "x" + RESOLUTIONS[i][1];
58 // Checks for and communicate failures to user (logcat and popup).
59 private void check(boolean value, String message) {
63 Log.e("WEBRTC-CHECK", message);
64 AlertDialog alertDialog = new AlertDialog.Builder(context).create();
65 alertDialog.setTitle("WebRTC Error");
66 alertDialog.setMessage(message);
67 alertDialog.setButton(DialogInterface.BUTTON_POSITIVE,
69 new DialogInterface.OnClickListener() {
70 public void onClick(DialogInterface dialog, int which) {
79 // Converts device rotation to camera rotation. Rotation depends on if the
80 // camera is back facing and rotate with the device or front facing and
81 // rotating in the opposite direction of the device.
82 private static int rotationFromRealWorldUp(CameraInfo info,
84 int coarseDeviceOrientation =
85 (int)(Math.round((double)deviceRotation / 90) * 90) % 360;
86 if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
87 // The front camera rotates in the opposite direction of the
89 int inverseDeviceOrientation = 360 - coarseDeviceOrientation;
90 return (inverseDeviceOrientation + info.orientation) % 360;
92 return (coarseDeviceOrientation + info.orientation) % 360;
95 // Shared Audio/Video members.
96 private final Context context;
97 private String remoteIp;
98 private boolean enableTrace;
101 private VoiceEngine voe;
102 private int audioChannel;
103 private boolean audioEnabled;
104 private boolean voeRunning;
105 private int audioCodecIndex;
106 private int audioTxPort;
107 private int audioRxPort;
109 private boolean speakerEnabled;
110 private boolean headsetPluggedIn;
111 private boolean enableAgc;
112 private boolean enableNs;
113 private boolean enableAecm;
115 private BroadcastReceiver headsetListener;
117 private boolean audioRtpDump;
118 private boolean apmRecord;
121 private VideoEngine vie;
122 private int videoChannel;
123 private boolean receiveVideo;
124 private boolean sendVideo;
125 private boolean vieRunning;
126 private int videoCodecIndex;
127 private int resolutionIndex;
128 private int videoTxPort;
129 private int videoRxPort;
131 // Indexed by CameraInfo.CAMERA_FACING_{BACK,FRONT}.
132 private CameraInfo cameras[];
133 private boolean useFrontCamera;
134 private int currentCameraHandle;
135 private boolean enableNack;
136 // openGl, surfaceView or mediaCodec (integers.xml)
137 private int viewSelection;
138 private boolean videoRtpDump;
140 private SurfaceView svLocal;
141 private SurfaceView svRemote;
142 MediaCodecVideoDecoder externalCodec;
149 private int inHeight;
151 private OrientationEventListener orientationListener;
152 private int deviceOrientation = OrientationEventListener.ORIENTATION_UNKNOWN;
154 public MediaEngine(Context context) {
155 this.context = context;
156 voe = new VoiceEngine();
157 check(voe.init() == 0, "Failed voe Init");
158 audioChannel = voe.createChannel();
159 check(audioChannel >= 0, "Failed voe CreateChannel");
160 vie = new VideoEngine();
161 check(vie.init() == 0, "Failed voe Init");
162 check(vie.setVoiceEngine(voe) == 0, "Failed setVoiceEngine");
163 videoChannel = vie.createChannel();
164 check(audioChannel >= 0, "Failed voe CreateChannel");
165 check(vie.connectAudioChannel(videoChannel, audioChannel) == 0,
166 "Failed ConnectAudioChannel");
168 cameras = new CameraInfo[2];
169 CameraInfo info = new CameraInfo();
170 for (int i = 0; i < Camera.getNumberOfCameras(); ++i) {
171 Camera.getCameraInfo(i, info);
172 cameras[info.facing] = info;
175 check(voe.setSpeakerVolume(volumeLevel) == 0,
176 "Failed setSpeakerVolume");
177 check(voe.setAecmMode(VoiceEngine.AecmModes.SPEAKERPHONE, false) == 0,
178 "VoE set Aecm speakerphone mode failed");
179 check(vie.setKeyFrameRequestMethod(videoChannel,
180 VideoEngine.VieKeyFrameRequestMethod.
181 KEY_FRAME_REQUEST_PLI_RTCP) == 0,
182 "Failed setKeyFrameRequestMethod");
183 check(vie.registerObserver(videoChannel, this) == 0,
184 "Failed registerObserver");
186 // TODO(hellner): SENSOR_DELAY_NORMAL?
187 // Listen to changes in device orientation.
188 orientationListener =
189 new OrientationEventListener(context, SensorManager.SENSOR_DELAY_UI) {
190 public void onOrientationChanged (int orientation) {
191 deviceOrientation = orientation;
192 compensateRotation();
195 orientationListener.enable();
196 // Listen to headset being plugged in/out.
197 IntentFilter receiverFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
198 headsetListener = new BroadcastReceiver() {
200 public void onReceive(Context context, Intent intent) {
201 if (intent.getAction().compareTo(Intent.ACTION_HEADSET_PLUG) == 0) {
202 headsetPluggedIn = intent.getIntExtra("state", 0) == 1;
207 context.registerReceiver(headsetListener, receiverFilter);
210 public void dispose() {
211 check(!voeRunning && !voeRunning, "Engines must be stopped before dispose");
212 context.unregisterReceiver(headsetListener);
213 orientationListener.disable();
214 check(vie.deregisterObserver(videoChannel) == 0,
215 "Failed deregisterObserver");
216 if (externalCodec != null) {
217 check(vie.deRegisterExternalReceiveCodec(videoChannel,
218 VCM_VP8_PAYLOAD_TYPE) == 0,
219 "Failed to deregister external decoder");
220 externalCodec = null;
222 check(vie.deleteChannel(videoChannel) == 0, "DeleteChannel");
224 check(voe.deleteChannel(audioChannel) == 0, "VoE delete channel failed");
228 public void start() {
232 if (receiveVideo || sendVideo) {
242 public boolean isRunning() {
243 return voeRunning || vieRunning;
246 public void setRemoteIp(String remoteIp) {
247 this.remoteIp = remoteIp;
248 UpdateSendDestination();
251 public String remoteIp() { return remoteIp; }
253 public void setTrace(boolean enable) {
255 vie.setTraceFile("/sdcard/trace.txt", false);
256 vie.setTraceFilter(VideoEngine.TraceLevel.TRACE_ERROR);
259 vie.setTraceFilter(VideoEngine.TraceLevel.TRACE_NONE);
262 private String getDebugDirectory() {
263 // Should create a folder in /scard/|LOG_DIR|
264 return Environment.getExternalStorageDirectory().toString() + "/" +
268 private boolean createDebugDirectory() {
269 File webrtc_dir = new File(getDebugDirectory());
270 if (!webrtc_dir.exists()) {
271 return webrtc_dir.mkdir();
273 return webrtc_dir.isDirectory();
276 public void startVoE() {
277 check(!voeRunning, "VoE already started");
278 check(voe.startListen(audioChannel) == 0, "Failed StartListen");
279 check(voe.startPlayout(audioChannel) == 0, "VoE start playout failed");
280 check(voe.startSend(audioChannel) == 0, "VoE start send failed");
284 private void stopVoe() {
285 check(voeRunning, "VoE not started");
286 check(voe.stopSend(audioChannel) == 0, "VoE stop send failed");
287 check(voe.stopPlayout(audioChannel) == 0, "VoE stop playout failed");
288 check(voe.stopListen(audioChannel) == 0, "VoE stop listen failed");
292 public void setAudio(boolean audioEnabled) {
293 this.audioEnabled = audioEnabled;
296 public boolean audioEnabled() { return audioEnabled; }
298 public int audioCodecIndex() { return audioCodecIndex; }
300 public void setAudioCodec(int codecNumber) {
301 audioCodecIndex = codecNumber;
302 CodecInst codec = voe.getCodec(codecNumber);
303 check(voe.setSendCodec(audioChannel, codec) == 0, "Failed setSendCodec");
307 public String[] audioCodecsAsString() {
308 String[] retVal = new String[voe.numOfCodecs()];
309 for (int i = 0; i < voe.numOfCodecs(); ++i) {
310 CodecInst codec = voe.getCodec(i);
311 retVal[i] = codec.toString();
317 private CodecInst[] defaultAudioCodecs() {
318 CodecInst[] retVal = new CodecInst[voe.numOfCodecs()];
319 for (int i = 0; i < voe.numOfCodecs(); ++i) {
320 retVal[i] = voe.getCodec(i);
325 public int getIsacIndex() {
326 CodecInst[] codecs = defaultAudioCodecs();
327 for (int i = 0; i < codecs.length; ++i) {
328 if (codecs[i].name().contains("ISAC")) {
335 public void setAudioTxPort(int audioTxPort) {
336 this.audioTxPort = audioTxPort;
337 UpdateSendDestination();
340 public int audioTxPort() { return audioTxPort; }
342 public void setAudioRxPort(int audioRxPort) {
343 check(voe.setLocalReceiver(audioChannel, audioRxPort) == 0,
344 "Failed setLocalReceiver");
345 this.audioRxPort = audioRxPort;
348 public int audioRxPort() { return audioRxPort; }
350 public boolean agcEnabled() { return enableAgc; }
352 public void setAgc(boolean enable) {
354 VoiceEngine.AgcConfig agc_config =
355 new VoiceEngine.AgcConfig(3, 9, true);
356 check(voe.setAgcConfig(agc_config) == 0, "VoE set AGC Config failed");
357 check(voe.setAgcStatus(enableAgc, VoiceEngine.AgcModes.FIXED_DIGITAL) == 0,
358 "VoE set AGC Status failed");
361 public boolean nsEnabled() { return enableNs; }
363 public void setNs(boolean enable) {
365 check(voe.setNsStatus(enableNs,
366 VoiceEngine.NsModes.MODERATE_SUPPRESSION) == 0,
367 "VoE set NS Status failed");
370 public boolean aecmEnabled() { return enableAecm; }
372 public void setEc(boolean enable) {
374 check(voe.setEcStatus(enable, VoiceEngine.EcModes.AECM) == 0,
378 public boolean speakerEnabled() {
379 return speakerEnabled;
382 public void setSpeaker(boolean enable) {
383 speakerEnabled = enable;
388 public boolean apmRecord() { return apmRecord; }
390 public boolean audioRtpDump() { return audioRtpDump; }
392 public void setDebuging(boolean enable) {
395 check(voe.stopDebugRecording() == 0, "Failed stopping debug");
398 if (!createDebugDirectory()) {
399 check(false, "Unable to create debug directory.");
402 String debugDirectory = getDebugDirectory();
403 check(voe.startDebugRecording(debugDirectory + String.format("/apm_%d.dat",
404 System.currentTimeMillis())) == 0,
405 "Failed starting debug");
408 public void setIncomingVoeRtpDump(boolean enable) {
409 audioRtpDump = enable;
411 check(voe.stopRtpDump(videoChannel,
412 VoiceEngine.RtpDirections.INCOMING) == 0,
413 "voe stopping rtp dump");
416 String debugDirectory = getDebugDirectory();
417 check(voe.startRtpDump(videoChannel, debugDirectory +
418 String.format("/voe_%d.rtp", System.currentTimeMillis()),
419 VoiceEngine.RtpDirections.INCOMING) == 0,
420 "voe starting rtp dump");
423 private void updateAudioOutput() {
424 boolean useSpeaker = !headsetPluggedIn && speakerEnabled;
425 check(voe.setLoudspeakerStatus(useSpeaker) == 0,
426 "Failed updating loudspeaker");
429 public void startViE() {
430 check(!vieRunning, "ViE already started");
434 context.getResources().getInteger(R.integer.openGl)) {
435 svRemote = ViERenderer.CreateRenderer(context, true);
436 } else if (viewSelection ==
437 context.getResources().getInteger(R.integer.surfaceView)) {
438 svRemote = ViERenderer.CreateRenderer(context, false);
440 externalCodec = new MediaCodecVideoDecoder(context);
441 svRemote = externalCodec.getView();
443 if (externalCodec != null) {
444 check(vie.registerExternalReceiveCodec(videoChannel,
445 VCM_VP8_PAYLOAD_TYPE, externalCodec, true) == 0,
446 "Failed to register external decoder");
448 check(vie.addRenderer(videoChannel, svRemote,
449 0, 0, 0, 1, 1) == 0, "Failed AddRenderer");
450 check(vie.startRender(videoChannel) == 0, "Failed StartRender");
452 check(vie.startReceive(videoChannel) == 0, "Failed StartReceive");
456 check(vie.startSend(videoChannel) == 0, "Failed StartSend");
461 private void stopVie() {
465 check(vie.stopSend(videoChannel) == 0, "StopSend");
467 check(vie.stopReceive(videoChannel) == 0, "StopReceive");
468 if (externalCodec != null) {
469 check(vie.deRegisterExternalReceiveCodec(videoChannel,
470 VCM_VP8_PAYLOAD_TYPE) == 0,
471 "Failed to deregister external decoder");
472 externalCodec.dispose();
473 externalCodec = null;
475 check(vie.stopRender(videoChannel) == 0, "StopRender");
476 check(vie.removeRenderer(videoChannel) == 0, "RemoveRenderer");
482 public void setReceiveVideo(boolean receiveVideo) {
483 this.receiveVideo = receiveVideo;
486 public boolean receiveVideo() { return receiveVideo; }
488 public void setSendVideo(boolean sendVideo) { this.sendVideo = sendVideo; }
490 public boolean sendVideo() { return sendVideo; }
492 public int videoCodecIndex() { return videoCodecIndex; }
494 public void setVideoCodec(int codecNumber) {
495 videoCodecIndex = codecNumber;
499 public String[] videoCodecsAsString() {
500 String[] retVal = new String[vie.numberOfCodecs()];
501 for (int i = 0; i < vie.numberOfCodecs(); ++i) {
502 VideoCodecInst codec = vie.getCodec(i);
503 retVal[i] = codec.toString();
509 public int resolutionIndex() { return resolutionIndex; }
511 public void setResolutionIndex(int resolution) {
512 resolutionIndex = resolution;
516 private void updateVideoCodec() {
517 VideoCodecInst codec = getVideoCodec(videoCodecIndex, resolutionIndex);
518 check(vie.setSendCodec(videoChannel, codec) == 0, "Failed setReceiveCodec");
522 private VideoCodecInst getVideoCodec(int codecNumber, int resolution) {
523 VideoCodecInst retVal = vie.getCodec(codecNumber);
524 retVal.setStartBitRate(INIT_BITRATE_KBPS);
525 retVal.setMaxBitRate(MAX_BITRATE_KBPS);
526 retVal.setWidth(RESOLUTIONS[resolution][WIDTH_IDX]);
527 retVal.setHeight(RESOLUTIONS[resolution][HEIGHT_IDX]);
528 retVal.setMaxFrameRate(SEND_CODEC_FPS);
532 public void setVideoRxPort(int videoRxPort) {
533 this.videoRxPort = videoRxPort;
534 check(vie.setLocalReceiver(videoChannel, videoRxPort) == 0,
535 "Failed setLocalReceiver");
538 public int videoRxPort() { return videoRxPort; }
540 public void setVideoTxPort(int videoTxPort) {
541 this.videoTxPort = videoTxPort;
542 UpdateSendDestination();
545 private void UpdateSendDestination() {
546 if (remoteIp == null) {
549 if (audioTxPort != 0) {
550 check(voe.setSendDestination(audioChannel, audioTxPort,
551 remoteIp) == 0, "VoE set send destination failed");
553 if (videoTxPort != 0) {
554 check(vie.setSendDestination(videoChannel, videoTxPort, remoteIp) == 0,
555 "Failed setSendDestination");
559 public int videoTxPort() {
563 public boolean hasMultipleCameras() {
564 return Camera.getNumberOfCameras() > 1;
567 public boolean frontCameraIsSet() {
568 return useFrontCamera;
571 // Set default camera to front if there is a front camera.
572 private void setDefaultCamera() {
573 useFrontCamera = hasFrontCamera();
576 public void toggleCamera() {
580 useFrontCamera = !useFrontCamera;
586 private void startCamera() {
587 CameraDesc cameraInfo = vie.getCaptureDevice(getCameraId(getCameraIndex()));
588 currentCameraHandle = vie.allocateCaptureDevice(cameraInfo);
589 cameraInfo.dispose();
590 check(vie.connectCaptureDevice(currentCameraHandle, videoChannel) == 0,
591 "Failed to connect capture device");
592 // Camera and preview surface. Note, renderer must be created before
593 // calling StartCapture or |svLocal| won't be able to render.
594 svLocal = ViERenderer.CreateLocalRenderer(context);
595 check(vie.startCapture(currentCameraHandle) == 0, "Failed StartCapture");
596 compensateRotation();
599 private void stopCamera() {
600 check(vie.stopCapture(currentCameraHandle) == 0, "Failed StopCapture");
602 check(vie.releaseCaptureDevice(currentCameraHandle) == 0,
603 "Failed ReleaseCaptureDevice");
606 private boolean hasFrontCamera() {
607 return cameras[CameraInfo.CAMERA_FACING_FRONT] != null;
610 public SurfaceView getRemoteSurfaceView() {
614 public SurfaceView getLocalSurfaceView() {
618 public void setViewSelection(int viewSelection) {
619 this.viewSelection = viewSelection;
622 public int viewSelection() { return viewSelection; }
624 public boolean nackEnabled() { return enableNack; }
626 public void setNack(boolean enable) {
628 check(vie.setNackStatus(videoChannel, enableNack) == 0,
629 "Failed setNackStatus");
632 // Collates current state into a multiline string.
633 public String sendReceiveState() {
636 RtcpStatistics stats = vie.getReceivedRtcpStatistics(videoChannel);
638 // Calculate % lost from fraction lost.
639 // Definition of fraction lost can be found in RFC3550.
640 packetLoss = (stats.fractionLost * 100) >> 8;
644 "fps in/out: " + inFps + "/" + outFps + "\n" +
645 "kBps in/out: " + inKbps / 1024 + "/ " + outKbps / 1024 + "\n" +
646 "resolution: " + inWidth + "x" + inHeight + "\n" +
647 "loss: " + packetLoss + "%";
651 MediaEngineObserver observer;
652 public void setObserver(MediaEngineObserver observer) {
653 this.observer = observer;
656 // Callbacks from the VideoDecodeEncodeObserver interface.
657 public void incomingRate(int videoChannel, int framerate, int bitrate) {
663 public void incomingCodecChanged(int videoChannel,
664 VideoCodecInst videoCodec) {
665 inWidth = videoCodec.width();
666 inHeight = videoCodec.height();
667 videoCodec.dispose();
671 public void requestNewKeyFrame(int videoChannel) {}
673 public void outgoingRate(int videoChannel, int framerate, int bitrate) {
679 private void newStats() {
680 if (observer != null) {
681 observer.newStats(sendReceiveState());
686 public boolean videoRtpDump() { return videoRtpDump; }
688 public void setIncomingVieRtpDump(boolean enable) {
689 videoRtpDump = enable;
691 check(vie.stopRtpDump(videoChannel,
692 VideoEngine.RtpDirections.INCOMING) == 0,
696 String debugDirectory = getDebugDirectory();
697 check(vie.startRtpDump(videoChannel, debugDirectory +
698 String.format("/vie_%d.rtp", System.currentTimeMillis()),
699 VideoEngine.RtpDirections.INCOMING) == 0,
703 private int getCameraIndex() {
704 return useFrontCamera ? Camera.CameraInfo.CAMERA_FACING_FRONT :
705 Camera.CameraInfo.CAMERA_FACING_BACK;
708 private int getCameraId(int index) {
709 for (int i = Camera.getNumberOfCameras() - 1; i >= 0; --i) {
710 CameraInfo info = new CameraInfo();
711 Camera.getCameraInfo(i, info);
712 if (index == info.facing) {
716 throw new RuntimeException("Index does not match a camera");
719 private void compensateRotation() {
720 if (svLocal == null) {
721 // Not rendering (or sending).
724 if (deviceOrientation == OrientationEventListener.ORIENTATION_UNKNOWN) {
727 int cameraRotation = rotationFromRealWorldUp(
728 cameras[getCameraIndex()], deviceOrientation);
729 // Egress streams should have real world up as up.
731 vie.setRotateCapturedFrames(currentCameraHandle, cameraRotation) == 0,
732 "Failed setRotateCapturedFrames: camera " + currentCameraHandle +
733 "rotation " + cameraRotation);