diff --git a/talk/examples/android/res/layout/fragment_menubar.xml b/talk/examples/android/res/layout/fragment_menubar.xml index 6d40138c60..1ff0a97261 100644 --- a/talk/examples/android/res/layout/fragment_menubar.xml +++ b/talk/examples/android/res/layout/fragment_menubar.xml @@ -18,6 +18,13 @@ + + Connecting to: %1$s FATAL ERROR: Missing URL to connect to. OK + Switch front/back camera Settings diff --git a/talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java b/talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java index 01ae7348df..8a875b205c 100644 --- a/talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java +++ b/talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java @@ -139,6 +139,14 @@ public class AppRTCDemoActivity extends Activity } }); + ((ImageButton) findViewById(R.id.button_switch_camera)).setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + pc.switchCamera(); + } + }); + ((ImageButton) findViewById(R.id.button_toggle_debug)).setOnClickListener( new View.OnClickListener() { @Override diff --git a/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java b/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java index 03760002b6..68159989f3 100644 --- a/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java +++ b/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java @@ -60,13 +60,17 @@ public class PeerConnectionClient { private boolean videoSourceStopped; private final PCObserver pcObserver = new PCObserver(); private final SDPObserver sdpObserver = new SDPObserver(); + private final MediaConstraints videoConstraints; + private final VideoRenderer.Callbacks localRender; private final VideoRenderer.Callbacks remoteRender; private LinkedList queuedRemoteCandidates = new LinkedList(); - private MediaConstraints sdpMediaConstraints; - private PeerConnectionEvents events; + private final MediaConstraints sdpMediaConstraints; + private final PeerConnectionEvents events; private boolean isInitiator; + private boolean useFrontFacingCamera = true; private SessionDescription localSdp = null; // either offer or answer SDP + private MediaStream videoMediaStream = null; public PeerConnectionClient( Activity activity, @@ -75,6 +79,8 @@ public class PeerConnectionClient { AppRTCSignalingParameters appRtcParameters, PeerConnectionEvents events) { this.activity = activity; + this.videoConstraints = appRtcParameters.videoConstraints; + this.localRender = localRender; this.remoteRender = remoteRender; this.events = events; isInitiator = appRtcParameters.initiator; @@ -101,23 +107,19 @@ public class PeerConnectionClient { // EnumSet.of(Logging.TraceLevel.TRACE_ALL), // Logging.Severity.LS_SENSITIVE); - Log.d(TAG, "Creating local video source"); - MediaStream lMS = factory.createLocalMediaStream("ARDAMS"); - if (appRtcParameters.videoConstraints != null) { - VideoCapturer capturer = getVideoCapturer(); - videoSource = factory.createVideoSource( - capturer, appRtcParameters.videoConstraints); - VideoTrack videoTrack = - factory.createVideoTrack("ARDAMSv0", videoSource); - videoTrack.addRenderer(new VideoRenderer(localRender)); - lMS.addTrack(videoTrack); + if (videoConstraints != null) { + videoMediaStream = factory.createLocalMediaStream("ARDAMSVideo"); + videoMediaStream.addTrack(createVideoTrack(useFrontFacingCamera)); + pc.addStream(videoMediaStream, new MediaConstraints()); } + if (appRtcParameters.audioConstraints != null) { + MediaStream lMS = factory.createLocalMediaStream("ARDAMSAudio"); lMS.addTrack(factory.createAudioTrack( "ARDAMSa0", factory.createAudioSource(appRtcParameters.audioConstraints))); + pc.addStream(lMS, new MediaConstraints()); } - pc.addStream(lMS, new MediaConstraints()); } public boolean getStats(StatsObserver observer, MediaStreamTrack track) { @@ -202,11 +204,15 @@ public class PeerConnectionClient { // Cycle through likely device names for the camera and return the first // capturer that works, or crash if none do. - private VideoCapturer getVideoCapturer() { + private VideoCapturer getVideoCapturer(boolean useFrontFacing) { String[] cameraFacing = { "front", "back" }; - int[] cameraIndex = { 0, 1 }; - int[] cameraOrientation = { 0, 90, 180, 270 }; + if (!useFrontFacing) { + cameraFacing[0] = "back"; + cameraFacing[1] = "front"; + } for (String facing : cameraFacing) { + int[] cameraIndex = { 0, 1 }; + int[] cameraOrientation = { 0, 90, 180, 270 }; for (int index : cameraIndex) { for (int orientation : cameraOrientation) { String name = "Camera " + index + ", Facing " + facing + @@ -222,6 +228,22 @@ public class PeerConnectionClient { throw new RuntimeException("Failed to open capturer"); } + private VideoTrack createVideoTrack(boolean frontFacing) { + VideoCapturer capturer = getVideoCapturer(frontFacing); + if (videoSource != null) { + videoSource.stop(); + videoSource.dispose(); + } + + videoSource = factory.createVideoSource( + capturer, videoConstraints); + String trackExtension = frontFacing ? "frontFacing" : "backFacing"; + VideoTrack videoTrack = + factory.createVideoTrack("ARDAMSv0" + trackExtension, videoSource); + videoTrack.addRenderer(new VideoRenderer(localRender)); + return videoTrack; + } + // Poor-man's assert(): die with |msg| unless |condition| is true. private static void abortUnless(boolean condition, String msg) { if (!condition) { @@ -285,6 +307,49 @@ public class PeerConnectionClient { queuedRemoteCandidates = null; } + public void switchCamera() { + if (videoConstraints == null) + return; // No video is sent. + + if (pc.signalingState() != PeerConnection.SignalingState.STABLE) { + Log.e(TAG, "Switching camera during negotiation is not handled."); + return; + } + + pc.removeStream(videoMediaStream); + VideoTrack currentTrack = videoMediaStream.videoTracks.get(0); + videoMediaStream.removeTrack(currentTrack); + + String trackId = currentTrack.id(); + // On Android, there can only be one camera open at the time and we + // need to release our implicit references to the videoSource before the + // PeerConnectionFactory is released. Since createVideoTrack creates a new + // videoSource and frees the old one, we need to release the track here. + currentTrack.dispose(); + + useFrontFacingCamera = !useFrontFacingCamera; + VideoTrack newTrack = createVideoTrack(useFrontFacingCamera); + videoMediaStream.addTrack(newTrack); + pc.addStream(videoMediaStream, new MediaConstraints()); + + SessionDescription remoteDesc = pc.getRemoteDescription(); + if (localSdp == null || remoteDesc == null) { + Log.d(TAG, "Switching camera before the negotiation started."); + return; + } + + localSdp = new SessionDescription(localSdp.type, + localSdp.description.replaceAll(trackId, newTrack.id())); + + if (isInitiator) { + pc.setLocalDescription(new SwitchCameraSdbObserver(), localSdp); + pc.setRemoteDescription(new SwitchCameraSdbObserver(), remoteDesc); + } else { + pc.setRemoteDescription(new SwitchCameraSdbObserver(), remoteDesc); + pc.setLocalDescription(new SwitchCameraSdbObserver(), localSdp); + } + } + // Implementation detail: observe ICE & stream changes and react accordingly. private class PCObserver implements PeerConnection.Observer { @Override @@ -442,4 +507,28 @@ public class PeerConnectionClient { }); } } + + private class SwitchCameraSdbObserver implements SdpObserver { + @Override + public void onCreateSuccess(SessionDescription sdp) { + } + + @Override + public void onSetSuccess() { + Log.d(TAG, "Camera switch SDP set succesfully"); + } + + @Override + public void onCreateFailure(final String error) { + } + + @Override + public void onSetFailure(final String error) { + activity.runOnUiThread(new Runnable() { + public void run() { + throw new RuntimeException("setSDP error while switching camera: " + error); + } + }); + } + } }