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;
14 import org.webrtc.videoengine.VideoCaptureAndroid;
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.os.Environment;
26 import android.util.Log;
27 import android.view.OrientationEventListener;
28 import android.view.SurfaceView;
31 public class MediaEngine implements VideoDecodeEncodeObserver {
32 // TODO(henrike): Most of these should be moved to xml (since static).
33 private static final int VCM_VP8_PAYLOAD_TYPE = 100;
34 private static final int SEND_CODEC_FPS = 30;
35 // TODO(henrike): increase INIT_BITRATE_KBPS to 2000 and ensure that
36 // 720p30fps can be acheived (on hardware that can handle it). Note that
37 // setting 2000 currently leads to failure, so that has to be resolved first.
38 private static final int INIT_BITRATE_KBPS = 500;
39 private static final int MAX_BITRATE_KBPS = 3000;
40 private static final String LOG_DIR = "webrtc";
41 private static final int WIDTH_IDX = 0;
42 private static final int HEIGHT_IDX = 1;
43 private static final int[][] RESOLUTIONS = {
44 {176,144}, {320,240}, {352,288}, {640,480}, {1280,720}
46 // Arbitrary choice of 4/5 volume (204/256).
47 private static final int volumeLevel = 204;
49 public static int numberOfResolutions() { return RESOLUTIONS.length; }
51 public static String[] resolutionsAsString() {
52 String[] retVal = new String[numberOfResolutions()];
53 for (int i = 0; i < numberOfResolutions(); ++i) {
54 retVal[i] = RESOLUTIONS[i][0] + "x" + RESOLUTIONS[i][1];
59 // Checks for and communicate failures to user (logcat and popup).
60 private void check(boolean value, String message) {
64 Log.e("WEBRTC-CHECK", message);
65 AlertDialog alertDialog = new AlertDialog.Builder(context).create();
66 alertDialog.setTitle("WebRTC Error");
67 alertDialog.setMessage(message);
68 alertDialog.setButton(DialogInterface.BUTTON_POSITIVE,
70 new DialogInterface.OnClickListener() {
71 public void onClick(DialogInterface dialog, int which) {
80 // Converts device rotation to camera rotation. Rotation depends on if the
81 // camera is back facing and rotate with the device or front facing and
82 // rotating in the opposite direction of the device.
83 private static int rotationFromRealWorldUp(CameraInfo info,
85 int coarseDeviceOrientation =
86 (int)(Math.round((double)deviceRotation / 90) * 90) % 360;
87 if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
88 // The front camera rotates in the opposite direction of the
90 int inverseDeviceOrientation = 360 - coarseDeviceOrientation;
91 return (inverseDeviceOrientation + info.orientation) % 360;
93 return (coarseDeviceOrientation + info.orientation) % 360;
96 // Shared Audio/Video members.
97 private final Context context;
98 private String remoteIp;
99 private boolean enableTrace;
102 private VoiceEngine voe;
103 private int audioChannel;
104 private boolean audioEnabled;
105 private boolean voeRunning;
106 private int audioCodecIndex;
107 private int audioTxPort;
108 private int audioRxPort;
110 private boolean speakerEnabled;
111 private boolean headsetPluggedIn;
112 private boolean enableAgc;
113 private boolean enableNs;
114 private boolean enableAecm;
116 private BroadcastReceiver headsetListener;
118 private boolean audioRtpDump;
119 private boolean apmRecord;
122 private VideoEngine vie;
123 private int videoChannel;
124 private boolean receiveVideo;
125 private boolean sendVideo;
126 private boolean vieRunning;
127 private int videoCodecIndex;
128 private int resolutionIndex;
129 private int videoTxPort;
130 private int videoRxPort;
132 // Indexed by CameraInfo.CAMERA_FACING_{BACK,FRONT}.
133 private CameraInfo cameras[];
134 private boolean useFrontCamera;
135 private int currentCameraHandle;
136 private boolean enableNack;
137 // openGl, surfaceView or mediaCodec (integers.xml)
138 private int viewSelection;
139 private boolean videoRtpDump;
141 private SurfaceView svLocal;
142 private SurfaceView svRemote;
143 MediaCodecVideoDecoder externalCodec;
150 private int inHeight;
152 private OrientationEventListener orientationListener;
153 private int deviceOrientation = OrientationEventListener.ORIENTATION_UNKNOWN;
155 public MediaEngine(Context context) {
156 this.context = context;
157 voe = new VoiceEngine();
158 check(voe.init() == 0, "Failed voe Init");
159 audioChannel = voe.createChannel();
160 check(audioChannel >= 0, "Failed voe CreateChannel");
161 vie = new VideoEngine();
162 check(vie.init() == 0, "Failed voe Init");
163 check(vie.setVoiceEngine(voe) == 0, "Failed setVoiceEngine");
164 videoChannel = vie.createChannel();
165 check(audioChannel >= 0, "Failed voe CreateChannel");
166 check(vie.connectAudioChannel(videoChannel, audioChannel) == 0,
167 "Failed ConnectAudioChannel");
169 cameras = new CameraInfo[2];
170 CameraInfo info = new CameraInfo();
171 for (int i = 0; i < Camera.getNumberOfCameras(); ++i) {
172 Camera.getCameraInfo(i, info);
173 cameras[info.facing] = info;
176 check(voe.setSpeakerVolume(volumeLevel) == 0,
177 "Failed setSpeakerVolume");
178 check(voe.setAecmMode(VoiceEngine.AecmModes.SPEAKERPHONE, false) == 0,
179 "VoE set Aecm speakerphone mode failed");
180 check(vie.setKeyFrameRequestMethod(videoChannel,
181 VideoEngine.VieKeyFrameRequestMethod.
182 KEY_FRAME_REQUEST_PLI_RTCP) == 0,
183 "Failed setKeyFrameRequestMethod");
184 check(vie.registerObserver(videoChannel, this) == 0,
185 "Failed registerObserver");
187 // TODO(hellner): SENSOR_DELAY_NORMAL?
188 // Listen to changes in device orientation.
189 orientationListener =
190 new OrientationEventListener(context, SensorManager.SENSOR_DELAY_UI) {
191 public void onOrientationChanged (int orientation) {
192 deviceOrientation = orientation;
193 compensateRotation();
196 orientationListener.enable();
197 // Listen to headset being plugged in/out.
198 IntentFilter receiverFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
199 headsetListener = new BroadcastReceiver() {
201 public void onReceive(Context context, Intent intent) {
202 if (intent.getAction().compareTo(Intent.ACTION_HEADSET_PLUG) == 0) {
203 headsetPluggedIn = intent.getIntExtra("state", 0) == 1;
208 context.registerReceiver(headsetListener, receiverFilter);
211 public void dispose() {
212 check(!voeRunning && !voeRunning, "Engines must be stopped before dispose");
213 context.unregisterReceiver(headsetListener);
214 orientationListener.disable();
215 check(vie.deregisterObserver(videoChannel) == 0,
216 "Failed deregisterObserver");
217 if (externalCodec != null) {
218 check(vie.deRegisterExternalReceiveCodec(videoChannel,
219 VCM_VP8_PAYLOAD_TYPE) == 0,
220 "Failed to deregister external decoder");
221 externalCodec = null;
223 check(vie.deleteChannel(videoChannel) == 0, "DeleteChannel");
225 check(voe.deleteChannel(audioChannel) == 0, "VoE delete channel failed");
229 public void start() {
233 if (receiveVideo || sendVideo) {
243 public boolean isRunning() {
244 return voeRunning || vieRunning;
247 public void setRemoteIp(String remoteIp) {
248 this.remoteIp = remoteIp;
249 UpdateSendDestination();
252 public String remoteIp() { return remoteIp; }
254 public void setTrace(boolean enable) {
256 vie.setTraceFile("/sdcard/trace.txt", false);
257 vie.setTraceFilter(VideoEngine.TraceLevel.TRACE_ERROR);
260 vie.setTraceFilter(VideoEngine.TraceLevel.TRACE_NONE);
263 private String getDebugDirectory() {
264 // Should create a folder in /scard/|LOG_DIR|
265 return Environment.getExternalStorageDirectory().toString() + "/" +
269 private boolean createDebugDirectory() {
270 File webrtc_dir = new File(getDebugDirectory());
271 if (!webrtc_dir.exists()) {
272 return webrtc_dir.mkdir();
274 return webrtc_dir.isDirectory();
277 public void startVoE() {
278 check(!voeRunning, "VoE already started");
279 check(voe.startListen(audioChannel) == 0, "Failed StartListen");
280 check(voe.startPlayout(audioChannel) == 0, "VoE start playout failed");
281 check(voe.startSend(audioChannel) == 0, "VoE start send failed");
285 private void stopVoe() {
286 check(voeRunning, "VoE not started");
287 check(voe.stopSend(audioChannel) == 0, "VoE stop send failed");
288 check(voe.stopPlayout(audioChannel) == 0, "VoE stop playout failed");
289 check(voe.stopListen(audioChannel) == 0, "VoE stop listen failed");
293 public void setAudio(boolean audioEnabled) {
294 this.audioEnabled = audioEnabled;
297 public boolean audioEnabled() { return audioEnabled; }
299 public int audioCodecIndex() { return audioCodecIndex; }
301 public void setAudioCodec(int codecNumber) {
302 audioCodecIndex = codecNumber;
303 CodecInst codec = voe.getCodec(codecNumber);
304 check(voe.setSendCodec(audioChannel, codec) == 0, "Failed setSendCodec");
308 public String[] audioCodecsAsString() {
309 String[] retVal = new String[voe.numOfCodecs()];
310 for (int i = 0; i < voe.numOfCodecs(); ++i) {
311 CodecInst codec = voe.getCodec(i);
312 retVal[i] = codec.toString();
318 private CodecInst[] defaultAudioCodecs() {
319 CodecInst[] retVal = new CodecInst[voe.numOfCodecs()];
320 for (int i = 0; i < voe.numOfCodecs(); ++i) {
321 retVal[i] = voe.getCodec(i);
326 public int getIsacIndex() {
327 CodecInst[] codecs = defaultAudioCodecs();
328 for (int i = 0; i < codecs.length; ++i) {
329 if (codecs[i].name().contains("ISAC")) {
336 public void setAudioTxPort(int audioTxPort) {
337 this.audioTxPort = audioTxPort;
338 UpdateSendDestination();
341 public int audioTxPort() { return audioTxPort; }
343 public void setAudioRxPort(int audioRxPort) {
344 check(voe.setLocalReceiver(audioChannel, audioRxPort) == 0,
345 "Failed setLocalReceiver");
346 this.audioRxPort = audioRxPort;
349 public int audioRxPort() { return audioRxPort; }
351 public boolean agcEnabled() { return enableAgc; }
353 public void setAgc(boolean enable) {
355 VoiceEngine.AgcConfig agc_config =
356 new VoiceEngine.AgcConfig(3, 9, true);
357 check(voe.setAgcConfig(agc_config) == 0, "VoE set AGC Config failed");
358 check(voe.setAgcStatus(enableAgc, VoiceEngine.AgcModes.FIXED_DIGITAL) == 0,
359 "VoE set AGC Status failed");
362 public boolean nsEnabled() { return enableNs; }
364 public void setNs(boolean enable) {
366 check(voe.setNsStatus(enableNs,
367 VoiceEngine.NsModes.MODERATE_SUPPRESSION) == 0,
368 "VoE set NS Status failed");
371 public boolean aecmEnabled() { return enableAecm; }
373 public void setEc(boolean enable) {
375 check(voe.setEcStatus(enable, VoiceEngine.EcModes.AECM) == 0,
379 public boolean speakerEnabled() {
380 return speakerEnabled;
383 public void setSpeaker(boolean enable) {
384 speakerEnabled = enable;
389 public boolean apmRecord() { return apmRecord; }
391 public boolean audioRtpDump() { return audioRtpDump; }
393 public void setDebuging(boolean enable) {
396 check(voe.stopDebugRecording() == 0, "Failed stopping debug");
399 if (!createDebugDirectory()) {
400 check(false, "Unable to create debug directory.");
403 String debugDirectory = getDebugDirectory();
404 check(voe.startDebugRecording(debugDirectory + String.format("/apm_%d.dat",
405 System.currentTimeMillis())) == 0,
406 "Failed starting debug");
409 public void setIncomingVoeRtpDump(boolean enable) {
410 audioRtpDump = enable;
412 check(voe.stopRtpDump(videoChannel,
413 VoiceEngine.RtpDirections.INCOMING) == 0,
414 "voe stopping rtp dump");
417 String debugDirectory = getDebugDirectory();
418 check(voe.startRtpDump(videoChannel, debugDirectory +
419 String.format("/voe_%d.rtp", System.currentTimeMillis()),
420 VoiceEngine.RtpDirections.INCOMING) == 0,
421 "voe starting rtp dump");
424 private void updateAudioOutput() {
425 boolean useSpeaker = !headsetPluggedIn && speakerEnabled;
426 check(voe.setLoudspeakerStatus(useSpeaker) == 0,
427 "Failed updating loudspeaker");
430 public void startViE() {
431 check(!vieRunning, "ViE already started");
435 context.getResources().getInteger(R.integer.openGl)) {
436 svRemote = ViERenderer.CreateRenderer(context, true);
437 } else if (viewSelection ==
438 context.getResources().getInteger(R.integer.surfaceView)) {
439 svRemote = ViERenderer.CreateRenderer(context, false);
441 externalCodec = new MediaCodecVideoDecoder(context);
442 svRemote = externalCodec.getView();
444 if (externalCodec != null) {
445 check(vie.registerExternalReceiveCodec(videoChannel,
446 VCM_VP8_PAYLOAD_TYPE, externalCodec, true) == 0,
447 "Failed to register external decoder");
449 check(vie.addRenderer(videoChannel, svRemote,
450 0, 0, 0, 1, 1) == 0, "Failed AddRenderer");
451 check(vie.startRender(videoChannel) == 0, "Failed StartRender");
453 check(vie.startReceive(videoChannel) == 0, "Failed StartReceive");
457 check(vie.startSend(videoChannel) == 0, "Failed StartSend");
462 private void stopVie() {
466 check(vie.stopSend(videoChannel) == 0, "StopSend");
468 check(vie.stopReceive(videoChannel) == 0, "StopReceive");
469 if (externalCodec != null) {
470 check(vie.deRegisterExternalReceiveCodec(videoChannel,
471 VCM_VP8_PAYLOAD_TYPE) == 0,
472 "Failed to deregister external decoder");
473 externalCodec.dispose();
474 externalCodec = null;
476 check(vie.stopRender(videoChannel) == 0, "StopRender");
477 check(vie.removeRenderer(videoChannel) == 0, "RemoveRenderer");
483 public void setReceiveVideo(boolean receiveVideo) {
484 this.receiveVideo = receiveVideo;
487 public boolean receiveVideo() { return receiveVideo; }
489 public void setSendVideo(boolean sendVideo) { this.sendVideo = sendVideo; }
491 public boolean sendVideo() { return sendVideo; }
493 public int videoCodecIndex() { return videoCodecIndex; }
495 public void setVideoCodec(int codecNumber) {
496 videoCodecIndex = codecNumber;
500 public String[] videoCodecsAsString() {
501 String[] retVal = new String[vie.numberOfCodecs()];
502 for (int i = 0; i < vie.numberOfCodecs(); ++i) {
503 VideoCodecInst codec = vie.getCodec(i);
504 retVal[i] = codec.toString();
510 public int resolutionIndex() { return resolutionIndex; }
512 public void setResolutionIndex(int resolution) {
513 resolutionIndex = resolution;
517 private void updateVideoCodec() {
518 VideoCodecInst codec = getVideoCodec(videoCodecIndex, resolutionIndex);
519 check(vie.setSendCodec(videoChannel, codec) == 0, "Failed setReceiveCodec");
523 private VideoCodecInst getVideoCodec(int codecNumber, int resolution) {
524 VideoCodecInst retVal = vie.getCodec(codecNumber);
525 retVal.setStartBitRate(INIT_BITRATE_KBPS);
526 retVal.setMaxBitRate(MAX_BITRATE_KBPS);
527 retVal.setWidth(RESOLUTIONS[resolution][WIDTH_IDX]);
528 retVal.setHeight(RESOLUTIONS[resolution][HEIGHT_IDX]);
529 retVal.setMaxFrameRate(SEND_CODEC_FPS);
533 public void setVideoRxPort(int videoRxPort) {
534 this.videoRxPort = videoRxPort;
535 check(vie.setLocalReceiver(videoChannel, videoRxPort) == 0,
536 "Failed setLocalReceiver");
539 public int videoRxPort() { return videoRxPort; }
541 public void setVideoTxPort(int videoTxPort) {
542 this.videoTxPort = videoTxPort;
543 UpdateSendDestination();
546 private void UpdateSendDestination() {
547 if (remoteIp == null) {
550 if (audioTxPort != 0) {
551 check(voe.setSendDestination(audioChannel, audioTxPort,
552 remoteIp) == 0, "VoE set send destination failed");
554 if (videoTxPort != 0) {
555 check(vie.setSendDestination(videoChannel, videoTxPort, remoteIp) == 0,
556 "Failed setSendDestination");
560 public int videoTxPort() {
564 public boolean hasMultipleCameras() {
565 return Camera.getNumberOfCameras() > 1;
568 public boolean frontCameraIsSet() {
569 return useFrontCamera;
572 // Set default camera to front if there is a front camera.
573 private void setDefaultCamera() {
574 useFrontCamera = hasFrontCamera();
577 public void toggleCamera() {
581 useFrontCamera = !useFrontCamera;
587 private void startCamera() {
588 CameraDesc cameraInfo = vie.getCaptureDevice(getCameraId(getCameraIndex()));
589 currentCameraHandle = vie.allocateCaptureDevice(cameraInfo);
590 cameraInfo.dispose();
591 check(vie.connectCaptureDevice(currentCameraHandle, videoChannel) == 0,
592 "Failed to connect capture device");
593 // Camera and preview surface.
594 svLocal = new SurfaceView(context);
595 VideoCaptureAndroid.setLocalPreview(svLocal.getHolder());
596 check(vie.startCapture(currentCameraHandle) == 0, "Failed StartCapture");
597 compensateRotation();
600 private void stopCamera() {
601 check(vie.stopCapture(currentCameraHandle) == 0, "Failed StopCapture");
603 check(vie.releaseCaptureDevice(currentCameraHandle) == 0,
604 "Failed ReleaseCaptureDevice");
607 private boolean hasFrontCamera() {
608 return cameras[CameraInfo.CAMERA_FACING_FRONT] != null;
611 public SurfaceView getRemoteSurfaceView() {
615 public SurfaceView getLocalSurfaceView() {
619 public void setViewSelection(int viewSelection) {
620 this.viewSelection = viewSelection;
623 public int viewSelection() { return viewSelection; }
625 public boolean nackEnabled() { return enableNack; }
627 public void setNack(boolean enable) {
629 check(vie.setNackStatus(videoChannel, enableNack) == 0,
630 "Failed setNackStatus");
633 // Collates current state into a multiline string.
634 public String sendReceiveState() {
637 RtcpStatistics stats = vie.getReceivedRtcpStatistics(videoChannel);
639 // Calculate % lost from fraction lost.
640 // Definition of fraction lost can be found in RFC3550.
641 packetLoss = (stats.fractionLost * 100) >> 8;
645 "fps in/out: " + inFps + "/" + outFps + "\n" +
646 "kBps in/out: " + inKbps / 1024 + "/ " + outKbps / 1024 + "\n" +
647 "resolution: " + inWidth + "x" + inHeight + "\n" +
648 "loss: " + packetLoss + "%";
652 MediaEngineObserver observer;
653 public void setObserver(MediaEngineObserver observer) {
654 this.observer = observer;
657 // Callbacks from the VideoDecodeEncodeObserver interface.
658 public void incomingRate(int videoChannel, int framerate, int bitrate) {
664 public void incomingCodecChanged(int videoChannel,
665 VideoCodecInst videoCodec) {
666 inWidth = videoCodec.width();
667 inHeight = videoCodec.height();
668 videoCodec.dispose();
672 public void requestNewKeyFrame(int videoChannel) {}
674 public void outgoingRate(int videoChannel, int framerate, int bitrate) {
680 private void newStats() {
681 if (observer != null) {
682 observer.newStats(sendReceiveState());
687 public boolean videoRtpDump() { return videoRtpDump; }
689 public void setIncomingVieRtpDump(boolean enable) {
690 videoRtpDump = enable;
692 check(vie.stopRtpDump(videoChannel,
693 VideoEngine.RtpDirections.INCOMING) == 0,
697 String debugDirectory = getDebugDirectory();
698 check(vie.startRtpDump(videoChannel, debugDirectory +
699 String.format("/vie_%d.rtp", System.currentTimeMillis()),
700 VideoEngine.RtpDirections.INCOMING) == 0,
704 private int getCameraIndex() {
705 return useFrontCamera ? Camera.CameraInfo.CAMERA_FACING_FRONT :
706 Camera.CameraInfo.CAMERA_FACING_BACK;
709 private int getCameraId(int index) {
710 for (int i = Camera.getNumberOfCameras() - 1; i >= 0; --i) {
711 CameraInfo info = new CameraInfo();
712 Camera.getCameraInfo(i, info);
713 if (index == info.facing) {
717 throw new RuntimeException("Index does not match a camera");
720 private void compensateRotation() {
721 if (svLocal == null) {
722 // Not rendering (or sending).
725 if (deviceOrientation == OrientationEventListener.ORIENTATION_UNKNOWN) {
728 int cameraRotation = rotationFromRealWorldUp(
729 cameras[getCameraIndex()], deviceOrientation);
730 // Egress streams should have real world up as up.
732 vie.setRotateCapturedFrames(currentCameraHandle, cameraRotation) == 0,
733 "Failed setRotateCapturedFrames: camera " + currentCameraHandle +
734 "rotation " + cameraRotation);