Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / third_party / libjingle / source / talk / examples / android / src / org / appspot / apprtc / AppRTCDemoActivity.java
1 /*
2  * libjingle
3  * Copyright 2013, Google Inc.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are met:
7  *
8  *  1. Redistributions of source code must retain the above copyright notice,
9  *     this list of conditions and the following disclaimer.
10  *  2. Redistributions in binary form must reproduce the above copyright notice,
11  *     this list of conditions and the following disclaimer in the documentation
12  *     and/or other materials provided with the distribution.
13  *  3. The name of the author may not be used to endorse or promote products
14  *     derived from this software without specific prior written permission.
15  *
16  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
17  * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
18  * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
19  * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
22  * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
23  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
24  * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
25  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26  */
27
28 package org.appspot.apprtc;
29
30 import android.app.Activity;
31 import android.app.AlertDialog;
32 import android.content.DialogInterface;
33 import android.content.Intent;
34 import android.graphics.Color;
35 import android.graphics.Point;
36 import android.media.AudioManager;
37 import android.os.Bundle;
38 import android.util.Log;
39 import android.util.TypedValue;
40 import android.view.View;
41 import android.view.ViewGroup.LayoutParams;
42 import android.view.WindowManager;
43 import android.webkit.JavascriptInterface;
44 import android.widget.EditText;
45 import android.widget.TextView;
46 import android.widget.Toast;
47
48 import org.json.JSONException;
49 import org.json.JSONObject;
50 import org.webrtc.DataChannel;
51 import org.webrtc.IceCandidate;
52 import org.webrtc.Logging;
53 import org.webrtc.MediaConstraints;
54 import org.webrtc.MediaStream;
55 import org.webrtc.PeerConnection;
56 import org.webrtc.PeerConnectionFactory;
57 import org.webrtc.SdpObserver;
58 import org.webrtc.SessionDescription;
59 import org.webrtc.StatsObserver;
60 import org.webrtc.StatsReport;
61 import org.webrtc.VideoCapturer;
62 import org.webrtc.VideoRenderer;
63 import org.webrtc.VideoRenderer.I420Frame;
64 import org.webrtc.VideoSource;
65 import org.webrtc.VideoTrack;
66
67 import java.util.EnumSet;
68 import java.util.LinkedList;
69 import java.util.List;
70 import java.util.regex.Matcher;
71 import java.util.regex.Pattern;
72
73 /**
74  * Main Activity of the AppRTCDemo Android app demonstrating interoperability
75  * between the Android/Java implementation of PeerConnection and the
76  * apprtc.appspot.com demo webapp.
77  */
78 public class AppRTCDemoActivity extends Activity
79     implements AppRTCClient.IceServersObserver {
80   private static final String TAG = "AppRTCDemoActivity";
81   private static boolean factoryStaticInitialized;
82   private PeerConnectionFactory factory;
83   private VideoSource videoSource;
84   private boolean videoSourceStopped;
85   private PeerConnection pc;
86   private final PCObserver pcObserver = new PCObserver();
87   private final SDPObserver sdpObserver = new SDPObserver();
88   private final GAEChannelClient.MessageHandler gaeHandler = new GAEHandler();
89   private AppRTCClient appRtcClient = new AppRTCClient(this, gaeHandler, this);
90   private VideoStreamsView vsv;
91   private Toast logToast;
92   private final LayoutParams hudLayout =
93       new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
94   private TextView hudView;
95   private LinkedList<IceCandidate> queuedRemoteCandidates =
96       new LinkedList<IceCandidate>();
97   // Synchronize on quit[0] to avoid teardown-related crashes.
98   private final Boolean[] quit = new Boolean[] { false };
99   private MediaConstraints sdpMediaConstraints;
100
101   @Override
102   public void onCreate(Bundle savedInstanceState) {
103     super.onCreate(savedInstanceState);
104
105     Thread.setDefaultUncaughtExceptionHandler(
106         new UnhandledExceptionHandler(this));
107
108     getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
109     getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
110
111     Point displaySize = new Point();
112     getWindowManager().getDefaultDisplay().getSize(displaySize);
113     vsv = new VideoStreamsView(this, displaySize);
114     vsv.setOnClickListener(new View.OnClickListener() {
115         @Override public void onClick(View v) {
116           toggleHUD();
117         }
118       });
119     setContentView(vsv);
120     hudView = new TextView(this);
121     hudView.setTextColor(Color.BLACK);
122     hudView.setBackgroundColor(Color.WHITE);
123     hudView.setAlpha(0.4f);
124     hudView.setTextSize(TypedValue.COMPLEX_UNIT_PT, 5);
125     addContentView(hudView, hudLayout);
126
127     if (!factoryStaticInitialized) {
128       abortUnless(PeerConnectionFactory.initializeAndroidGlobals(this),
129         "Failed to initializeAndroidGlobals");
130       factoryStaticInitialized = true;
131     }
132
133     AudioManager audioManager =
134         ((AudioManager) getSystemService(AUDIO_SERVICE));
135     // TODO(fischman): figure out how to do this Right(tm) and remove the
136     // suppression.
137     @SuppressWarnings("deprecation")
138     boolean isWiredHeadsetOn = audioManager.isWiredHeadsetOn();
139     audioManager.setMode(isWiredHeadsetOn ?
140         AudioManager.MODE_IN_CALL : AudioManager.MODE_IN_COMMUNICATION);
141     audioManager.setSpeakerphoneOn(!isWiredHeadsetOn);
142
143     sdpMediaConstraints = new MediaConstraints();
144     sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
145         "OfferToReceiveAudio", "true"));
146     sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
147         "OfferToReceiveVideo", "true"));
148
149     final Intent intent = getIntent();
150     if ("android.intent.action.VIEW".equals(intent.getAction())) {
151       connectToRoom(intent.getData().toString());
152       return;
153     }
154     showGetRoomUI();
155   }
156
157   private void showGetRoomUI() {
158     final EditText roomInput = new EditText(this);
159     roomInput.setText("https://apprtc.appspot.com/?r=");
160     roomInput.setSelection(roomInput.getText().length());
161     DialogInterface.OnClickListener listener =
162         new DialogInterface.OnClickListener() {
163           @Override public void onClick(DialogInterface dialog, int which) {
164             abortUnless(which == DialogInterface.BUTTON_POSITIVE, "lolwat?");
165             dialog.dismiss();
166             connectToRoom(roomInput.getText().toString());
167           }
168         };
169     AlertDialog.Builder builder = new AlertDialog.Builder(this);
170     builder
171         .setMessage("Enter room URL").setView(roomInput)
172         .setPositiveButton("Go!", listener).show();
173   }
174
175   private void connectToRoom(String roomUrl) {
176     logAndToast("Connecting to room...");
177     appRtcClient.connectToRoom(roomUrl);
178   }
179
180   // Toggle visibility of the heads-up display.
181   private void toggleHUD() {
182     if (hudView.getVisibility() == View.VISIBLE) {
183       hudView.setVisibility(View.INVISIBLE);
184     } else {
185       hudView.setVisibility(View.VISIBLE);
186     }
187   }
188
189   // Update the heads-up display with information from |reports|.
190   private void updateHUD(StatsReport[] reports) {
191     if (hudView.getText().length() == 0) {
192       logAndToast("Tap the screen to toggle stats visibility");
193     }
194     StringBuilder builder = new StringBuilder();
195     for (StatsReport report : reports) {
196       if (!report.id.equals("bweforvideo")) {
197         continue;
198       }
199       for (StatsReport.Value value : report.values) {
200         String name = value.name.replace("goog", "").replace("Available", "")
201             .replace("Bandwidth", "").replace("Bitrate", "").replace("Enc", "");
202         builder.append(name).append("=").append(value.value).append(" ");
203       }
204       builder.append("\n");
205     }
206     hudView.setText(builder.toString() + hudView.getText());
207   }
208
209   @Override
210   public void onPause() {
211     super.onPause();
212     vsv.onPause();
213     if (videoSource != null) {
214       videoSource.stop();
215       videoSourceStopped = true;
216     }
217   }
218
219   @Override
220   public void onResume() {
221     super.onResume();
222     vsv.onResume();
223     if (videoSource != null && videoSourceStopped) {
224       videoSource.restart();
225     }
226   }
227
228
229   // Just for fun (and to regression-test bug 2302) make sure that DataChannels
230   // can be created, queried, and disposed.
231   private static void createDataChannelToRegressionTestBug2302(
232       PeerConnection pc) {
233     DataChannel dc = pc.createDataChannel("dcLabel", new DataChannel.Init());
234     abortUnless("dcLabel".equals(dc.label()), "Unexpected label corruption?");
235     dc.close();
236     dc.dispose();
237   }
238
239   @Override
240   public void onIceServers(List<PeerConnection.IceServer> iceServers) {
241     factory = new PeerConnectionFactory();
242
243     MediaConstraints pcConstraints = appRtcClient.pcConstraints();
244     pcConstraints.optional.add(
245         new MediaConstraints.KeyValuePair("RtpDataChannels", "true"));
246     pc = factory.createPeerConnection(iceServers, pcConstraints, pcObserver);
247
248     createDataChannelToRegressionTestBug2302(pc);  // See method comment.
249
250     // Uncomment to get ALL WebRTC tracing and SENSITIVE libjingle logging.
251     // NOTE: this _must_ happen while |factory| is alive!
252     // Logging.enableTracing(
253     //     "logcat:",
254     //     EnumSet.of(Logging.TraceLevel.TRACE_ALL),
255     //     Logging.Severity.LS_SENSITIVE);
256
257     {
258       final PeerConnection finalPC = pc;
259       final Runnable repeatedStatsLogger = new Runnable() {
260           public void run() {
261             synchronized (quit[0]) {
262               if (quit[0]) {
263                 return;
264               }
265               final Runnable runnableThis = this;
266               boolean success = finalPC.getStats(new StatsObserver() {
267                   public void onComplete(final StatsReport[] reports) {
268                     runOnUiThread(new Runnable() {
269                         public void run() {
270                           updateHUD(reports);
271                         }
272                       });
273                     for (StatsReport report : reports) {
274                       Log.d(TAG, "Stats: " + report.toString());
275                     }
276                     vsv.postDelayed(runnableThis, 1000);
277                   }
278                 }, null);
279               if (!success) {
280                 throw new RuntimeException("getStats() return false!");
281               }
282             }
283           }
284         };
285       vsv.postDelayed(repeatedStatsLogger, 1000);
286     }
287
288     {
289       logAndToast("Creating local video source...");
290       MediaStream lMS = factory.createLocalMediaStream("ARDAMS");
291       if (appRtcClient.videoConstraints() != null) {
292         VideoCapturer capturer = getVideoCapturer();
293         videoSource = factory.createVideoSource(
294             capturer, appRtcClient.videoConstraints());
295         VideoTrack videoTrack =
296             factory.createVideoTrack("ARDAMSv0", videoSource);
297         videoTrack.addRenderer(new VideoRenderer(new VideoCallbacks(
298             vsv, VideoStreamsView.Endpoint.LOCAL)));
299         lMS.addTrack(videoTrack);
300       }
301       if (appRtcClient.audioConstraints() != null) {
302         lMS.addTrack(factory.createAudioTrack(
303             "ARDAMSa0",
304             factory.createAudioSource(appRtcClient.audioConstraints())));
305       }
306       pc.addStream(lMS, new MediaConstraints());
307     }
308     logAndToast("Waiting for ICE candidates...");
309   }
310
311   // Cycle through likely device names for the camera and return the first
312   // capturer that works, or crash if none do.
313   private VideoCapturer getVideoCapturer() {
314     String[] cameraFacing = { "front", "back" };
315     int[] cameraIndex = { 0, 1 };
316     int[] cameraOrientation = { 0, 90, 180, 270 };
317     for (String facing : cameraFacing) {
318       for (int index : cameraIndex) {
319         for (int orientation : cameraOrientation) {
320           String name = "Camera " + index + ", Facing " + facing +
321               ", Orientation " + orientation;
322           VideoCapturer capturer = VideoCapturer.create(name);
323           if (capturer != null) {
324             logAndToast("Using camera: " + name);
325             return capturer;
326           }
327         }
328       }
329     }
330     throw new RuntimeException("Failed to open capturer");
331   }
332
333   @Override
334   protected void onDestroy() {
335     disconnectAndExit();
336     super.onDestroy();
337   }
338
339   // Poor-man's assert(): die with |msg| unless |condition| is true.
340   private static void abortUnless(boolean condition, String msg) {
341     if (!condition) {
342       throw new RuntimeException(msg);
343     }
344   }
345
346   // Log |msg| and Toast about it.
347   private void logAndToast(String msg) {
348     Log.d(TAG, msg);
349     if (logToast != null) {
350       logToast.cancel();
351     }
352     logToast = Toast.makeText(this, msg, Toast.LENGTH_SHORT);
353     logToast.show();
354   }
355
356   // Send |json| to the underlying AppEngine Channel.
357   private void sendMessage(JSONObject json) {
358     appRtcClient.sendMessage(json.toString());
359   }
360
361   // Put a |key|->|value| mapping in |json|.
362   private static void jsonPut(JSONObject json, String key, Object value) {
363     try {
364       json.put(key, value);
365     } catch (JSONException e) {
366       throw new RuntimeException(e);
367     }
368   }
369
370   // Mangle SDP to prefer ISAC/16000 over any other audio codec.
371   private static String preferISAC(String sdpDescription) {
372     String[] lines = sdpDescription.split("\r\n");
373     int mLineIndex = -1;
374     String isac16kRtpMap = null;
375     Pattern isac16kPattern =
376         Pattern.compile("^a=rtpmap:(\\d+) ISAC/16000[\r]?$");
377     for (int i = 0;
378          (i < lines.length) && (mLineIndex == -1 || isac16kRtpMap == null);
379          ++i) {
380       if (lines[i].startsWith("m=audio ")) {
381         mLineIndex = i;
382         continue;
383       }
384       Matcher isac16kMatcher = isac16kPattern.matcher(lines[i]);
385       if (isac16kMatcher.matches()) {
386         isac16kRtpMap = isac16kMatcher.group(1);
387         continue;
388       }
389     }
390     if (mLineIndex == -1) {
391       Log.d(TAG, "No m=audio line, so can't prefer iSAC");
392       return sdpDescription;
393     }
394     if (isac16kRtpMap == null) {
395       Log.d(TAG, "No ISAC/16000 line, so can't prefer iSAC");
396       return sdpDescription;
397     }
398     String[] origMLineParts = lines[mLineIndex].split(" ");
399     StringBuilder newMLine = new StringBuilder();
400     int origPartIndex = 0;
401     // Format is: m=<media> <port> <proto> <fmt> ...
402     newMLine.append(origMLineParts[origPartIndex++]).append(" ");
403     newMLine.append(origMLineParts[origPartIndex++]).append(" ");
404     newMLine.append(origMLineParts[origPartIndex++]).append(" ");
405     newMLine.append(isac16kRtpMap);
406     for (; origPartIndex < origMLineParts.length; ++origPartIndex) {
407       if (!origMLineParts[origPartIndex].equals(isac16kRtpMap)) {
408         newMLine.append(" ").append(origMLineParts[origPartIndex]);
409       }
410     }
411     lines[mLineIndex] = newMLine.toString();
412     StringBuilder newSdpDescription = new StringBuilder();
413     for (String line : lines) {
414       newSdpDescription.append(line).append("\r\n");
415     }
416     return newSdpDescription.toString();
417   }
418
419   // Implementation detail: observe ICE & stream changes and react accordingly.
420   private class PCObserver implements PeerConnection.Observer {
421     @Override public void onIceCandidate(final IceCandidate candidate){
422       runOnUiThread(new Runnable() {
423           public void run() {
424             JSONObject json = new JSONObject();
425             jsonPut(json, "type", "candidate");
426             jsonPut(json, "label", candidate.sdpMLineIndex);
427             jsonPut(json, "id", candidate.sdpMid);
428             jsonPut(json, "candidate", candidate.sdp);
429             sendMessage(json);
430           }
431         });
432     }
433
434     @Override public void onError(){
435       runOnUiThread(new Runnable() {
436           public void run() {
437             throw new RuntimeException("PeerConnection error!");
438           }
439         });
440     }
441
442     @Override public void onSignalingChange(
443         PeerConnection.SignalingState newState) {
444     }
445
446     @Override public void onIceConnectionChange(
447         PeerConnection.IceConnectionState newState) {
448     }
449
450     @Override public void onIceGatheringChange(
451         PeerConnection.IceGatheringState newState) {
452     }
453
454     @Override public void onAddStream(final MediaStream stream){
455       runOnUiThread(new Runnable() {
456           public void run() {
457             abortUnless(stream.audioTracks.size() <= 1 &&
458                 stream.videoTracks.size() <= 1,
459                 "Weird-looking stream: " + stream);
460             if (stream.videoTracks.size() == 1) {
461               stream.videoTracks.get(0).addRenderer(new VideoRenderer(
462                   new VideoCallbacks(vsv, VideoStreamsView.Endpoint.REMOTE)));
463             }
464           }
465         });
466     }
467
468     @Override public void onRemoveStream(final MediaStream stream){
469       runOnUiThread(new Runnable() {
470           public void run() {
471             stream.videoTracks.get(0).dispose();
472           }
473         });
474     }
475
476     @Override public void onDataChannel(final DataChannel dc) {
477       runOnUiThread(new Runnable() {
478           public void run() {
479             throw new RuntimeException(
480                 "AppRTC doesn't use data channels, but got: " + dc.label() +
481                 " anyway!");
482           }
483         });
484     }
485
486     @Override public void onRenegotiationNeeded() {
487       // No need to do anything; AppRTC follows a pre-agreed-upon
488       // signaling/negotiation protocol.
489     }
490   }
491
492   // Implementation detail: handle offer creation/signaling and answer setting,
493   // as well as adding remote ICE candidates once the answer SDP is set.
494   private class SDPObserver implements SdpObserver {
495     private SessionDescription localSdp;
496
497     @Override public void onCreateSuccess(final SessionDescription origSdp) {
498       abortUnless(localSdp == null, "multiple SDP create?!?");
499       final SessionDescription sdp = new SessionDescription(
500           origSdp.type, preferISAC(origSdp.description));
501       localSdp = sdp;
502       runOnUiThread(new Runnable() {
503           public void run() {
504             pc.setLocalDescription(sdpObserver, sdp);
505           }
506         });
507     }
508
509     // Helper for sending local SDP (offer or answer, depending on role) to the
510     // other participant.  Note that it is important to send the output of
511     // create{Offer,Answer} and not merely the current value of
512     // getLocalDescription() because the latter may include ICE candidates that
513     // we might want to filter elsewhere.
514     private void sendLocalDescription() {
515       logAndToast("Sending " + localSdp.type);
516       JSONObject json = new JSONObject();
517       jsonPut(json, "type", localSdp.type.canonicalForm());
518       jsonPut(json, "sdp", localSdp.description);
519       sendMessage(json);
520     }
521
522     @Override public void onSetSuccess() {
523       runOnUiThread(new Runnable() {
524           public void run() {
525             if (appRtcClient.isInitiator()) {
526               if (pc.getRemoteDescription() != null) {
527                 // We've set our local offer and received & set the remote
528                 // answer, so drain candidates.
529                 drainRemoteCandidates();
530               } else {
531                 // We've just set our local description so time to send it.
532                 sendLocalDescription();
533               }
534             } else {
535               if (pc.getLocalDescription() == null) {
536                 // We just set the remote offer, time to create our answer.
537                 logAndToast("Creating answer");
538                 pc.createAnswer(SDPObserver.this, sdpMediaConstraints);
539               } else {
540                 // Answer now set as local description; send it and drain
541                 // candidates.
542                 sendLocalDescription();
543                 drainRemoteCandidates();
544               }
545             }
546           }
547         });
548     }
549
550     @Override public void onCreateFailure(final String error) {
551       runOnUiThread(new Runnable() {
552           public void run() {
553             throw new RuntimeException("createSDP error: " + error);
554           }
555         });
556     }
557
558     @Override public void onSetFailure(final String error) {
559       runOnUiThread(new Runnable() {
560           public void run() {
561             throw new RuntimeException("setSDP error: " + error);
562           }
563         });
564     }
565
566     private void drainRemoteCandidates() {
567       for (IceCandidate candidate : queuedRemoteCandidates) {
568         pc.addIceCandidate(candidate);
569       }
570       queuedRemoteCandidates = null;
571     }
572   }
573
574   // Implementation detail: handler for receiving GAE messages and dispatching
575   // them appropriately.
576   private class GAEHandler implements GAEChannelClient.MessageHandler {
577     @JavascriptInterface public void onOpen() {
578       if (!appRtcClient.isInitiator()) {
579         return;
580       }
581       logAndToast("Creating offer...");
582       pc.createOffer(sdpObserver, sdpMediaConstraints);
583     }
584
585     @JavascriptInterface public void onMessage(String data) {
586       try {
587         JSONObject json = new JSONObject(data);
588         String type = (String) json.get("type");
589         if (type.equals("candidate")) {
590           IceCandidate candidate = new IceCandidate(
591               (String) json.get("id"),
592               json.getInt("label"),
593               (String) json.get("candidate"));
594           if (queuedRemoteCandidates != null) {
595             queuedRemoteCandidates.add(candidate);
596           } else {
597             pc.addIceCandidate(candidate);
598           }
599         } else if (type.equals("answer") || type.equals("offer")) {
600           SessionDescription sdp = new SessionDescription(
601               SessionDescription.Type.fromCanonicalForm(type),
602               preferISAC((String) json.get("sdp")));
603           pc.setRemoteDescription(sdpObserver, sdp);
604         } else if (type.equals("bye")) {
605           logAndToast("Remote end hung up; dropping PeerConnection");
606           disconnectAndExit();
607         } else {
608           throw new RuntimeException("Unexpected message: " + data);
609         }
610       } catch (JSONException e) {
611         throw new RuntimeException(e);
612       }
613     }
614
615     @JavascriptInterface public void onClose() {
616       disconnectAndExit();
617     }
618
619     @JavascriptInterface public void onError(int code, String description) {
620       disconnectAndExit();
621     }
622   }
623
624   // Disconnect from remote resources, dispose of local resources, and exit.
625   private void disconnectAndExit() {
626     synchronized (quit[0]) {
627       if (quit[0]) {
628         return;
629       }
630       quit[0] = true;
631       if (pc != null) {
632         pc.dispose();
633         pc = null;
634       }
635       if (appRtcClient != null) {
636         appRtcClient.sendMessage("{\"type\": \"bye\"}");
637         appRtcClient.disconnect();
638         appRtcClient = null;
639       }
640       if (videoSource != null) {
641         videoSource.dispose();
642         videoSource = null;
643       }
644       if (factory != null) {
645         factory.dispose();
646         factory = null;
647       }
648       finish();
649     }
650   }
651
652   // Implementation detail: bridge the VideoRenderer.Callbacks interface to the
653   // VideoStreamsView implementation.
654   private class VideoCallbacks implements VideoRenderer.Callbacks {
655     private final VideoStreamsView view;
656     private final VideoStreamsView.Endpoint stream;
657
658     public VideoCallbacks(
659         VideoStreamsView view, VideoStreamsView.Endpoint stream) {
660       this.view = view;
661       this.stream = stream;
662     }
663
664     @Override
665     public void setSize(final int width, final int height) {
666       view.queueEvent(new Runnable() {
667           public void run() {
668             view.setSize(stream, width, height);
669           }
670         });
671     }
672
673     @Override
674     public void renderFrame(I420Frame frame) {
675       view.queueFrame(stream, frame);
676     }
677   }
678 }