実際につなげてみる。
Android - iOS
(※Androidを1つしかもってないので相手はIosです)
WebRTCのビルド〜プロジェクトへの追加は 前回 webRTC for android vol1
TrickleICE は次回 webRTC for android vol3
Ios版とwampサーバーを作る記事は webRTC for ios vol2
#成果物
https://github.com/nakadoribooks/webrtc-android/releases/tag/v0.0.2
#参考
AppRTCDemo
設定
wampのライブラリは jawampa
Gradle
build.gradle(Project-android)
buildscript {
repositories {
jcenter()
maven {
url 'https://raw.github.com/Matthias247/jawampa/mvn-repo/'
}
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.1'
}
}
allprojects {
repositories {
jcenter()
maven {
url 'https://raw.github.com/Matthias247/jawampa/mvn-repo/'
}
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
build.gradle(Module-app)
apply plugin: 'com.android.application'
android {
compileSdkVersion 24
buildToolsVersion "25.0.2"
defaultConfig {
applicationId "com.nakadoribooks.webrtcexample"
minSdkVersion 21
targetSdkVersion 24
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
packagingOptions {
exclude 'META-INF/io.netty.versions.properties'
exclude 'META-INF/INDEX.LIST'
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE'
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile(name:'libwebrtc', ext:'aar')
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:24.2.1'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'
compile 'ws.wamp.jawampa:jawampa:0.2.0'
compile 'io.reactivex:rxandroid:0.24.0'
}
repositories{
flatDir{
dirs 'libs'
}
}
AndroidManifest.xml
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.nakadoribooks.webrtcexample">
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature
android:glEsVersion="0x00020000"
android:required="true" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Layout
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.nakadoribooks.webrtcexample.MainActivity">
<android.support.v7.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="12">
<FrameLayout
android:id="@+id/local_video_layout"
android:layout_width="100dp"
android:layout_height="150dp"
android:layout_marginTop="300dp"
android:layout_marginLeft="20dp"
android:background="@color/colorAccent">
<org.webrtc.SurfaceViewRenderer
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/local_render_view"
/>
</FrameLayout>
<FrameLayout
android:id="@+id/remote_video_layout"
android:layout_width="match_parent"
android:layout_height="350dp"
android:background="@color/colorPrimary">
<org.webrtc.SurfaceViewRenderer
android:id="@+id/remote_render_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</RelativeLayout>
<android.support.v7.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="0dp"
android:text=""
android:layout_weight="1"
android:textAlignment="center"
android:textSize="20dp"
android:id="@+id/status_label"/>
<android.support.v7.widget.AppCompatButton
android:layout_width="match_parent"
android:layout_height="0dp"
android:text="Connect"
android:id="@+id/control_button"
android:textSize="20dp"
android:layout_weight="2"/>
</android.support.v7.widget.LinearLayoutCompat>
</android.support.constraint.ConstraintLayout>
実装抜粋
WebRTC
WebRTC.java
// interface -----------------
public void connect(WebRTCCallbacks callbacks)
public void startCapture()
public void createOffer()
public void receiveOffer(String sdp)
public void receiveAnswer(String sdp)
// callbaks -----------------
public static interface WebRTCCallbacks{
void onCreateLocalSdp(String sdp);
void didReceiveRemoteStream();
}
Wamp
Wamp.java
// interface -------
public void connect(WampCallbacks callbacks)
public void publishOffer(String sdp)
public void publishAnswer(String sdp)
// callbacks --------
public static interface WampCallbacks{
void onConnected();
void onReceiveOffer(String sdp);
void onReceiveAnswer(String sdp);
}
アプリケーション
MainActivity.java
// state -------
private enum State{
Disconnected
, Connecting
, Connected
, Offering
, ReceivedOffer
, CreatingAnswer
, Done
}
// connect wamp --------
private void connect(){
wamp.connect(new Wamp.WampCallbacks() {
@Override
public void onConnected() {
changeState(State.Connected);
}
@Override
public void onReceiveOffer(String sdp) {
if(typeOffer){
return;
}
changeState(State.CreatingAnswer);
webRTC.receiveOffer(sdp);
}
@Override
public void onReceiveAnswer(String sdp) {
if(!typeOffer){
return;
}
webRTC.receiveAnswer(sdp);
}
});
}
// start webRTC -----
private void startWebRTC(){
webRTC = new WebRTC(this);
webRTC.connect(new WebRTC.WebRTCCallbacks() {
@Override
public void onCreateLocalSdp(String sdp) {
if(typeOffer){
wamp.publishOffer(sdp);
}else{
wamp.publishAnswer(sdp);
}
}
@Override
public void didReceiveRemoteStream() {
changeState(State.Done);
}
});
webRTC.startCapture();
}
実装詳細
WebRTC
WebRTC.java
package com.nakadoribooks.webrtcexample;
import android.app.Activity;
import android.content.Context;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.WindowManager;
import org.webrtc.*;
import java.util.Arrays;
import java.util.List;
/**
* Created by kawase on 2017/04/13.
*/
public class WebRTC implements PeerConnection.Observer {
public static interface WebRTCCallbacks{
void onCreateLocalSdp(String sdp);
void didReceiveRemoteStream();
}
private static abstract class SkeletalSdpObserver implements SdpObserver{
private static final String TAG = "SkeletalSdpObserver";
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {}
@Override
public void onSetSuccess() {}
@Override
public void onCreateFailure(String s) {}
@Override
public void onSetFailure(String s) {}
}
private static final String TAG = "WebRTC";
private final Activity activity;
private WebRTCCallbacks callbacks;
private PeerConnectionFactory factory;
private PeerConnection peerConnection;
private MediaStream localStream;
private VideoCapturer videoCapturer;
private EglBase eglBase;
private VideoTrack localVideoTrack;
private VideoRenderer localRenderer;
private VideoRenderer remoteRenderer;
WebRTC(Activity activity){
this.activity = activity;
}
// interface -----------------
public void connect(WebRTCCallbacks callbacks){
this.callbacks = callbacks;
setupPeerConnection();
setupLocalStream();
peerConnection.addStream(localStream);
}
public void startCapture(){
_startCapture();
}
public void createOffer(){
_createOffer();
}
public void receiveOffer(String sdp){
_receiveOffer(sdp);
}
public void receiveAnswer(String sdp){
_receiveAnswer(sdp);
}
// implements -------------
private void setupPeerConnection(){
// rendereContext
eglBase = EglBase.create();
// initialize Factory
PeerConnectionFactory.initializeAndroidGlobals(activity.getApplicationContext(), true);
PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
factory = new PeerConnectionFactory(options);
factory.setVideoHwAccelerationOptions(eglBase.getEglBaseContext(), eglBase.getEglBaseContext());
// create PeerConnection
List<PeerConnection.IceServer> iceServers = Arrays.asList(new PeerConnection.IceServer("stun:stun.l.google.com:19302"));
peerConnection = factory.createPeerConnection(iceServers, WebRTCUtil.peerConnectionConstraints(), this);
}
private void _receiveAnswer(String sdp){
SessionDescription remoteDescription = new SessionDescription(SessionDescription.Type.ANSWER, sdp);
peerConnection.setRemoteDescription(new SkeletalSdpObserver() {
@Override
public void onSetSuccess() {
}
}, remoteDescription);
}
private void _receiveOffer(String sdp){
// setRemoteDescription
SessionDescription remoteDescription = new SessionDescription(SessionDescription.Type.OFFER, sdp);
peerConnection.setRemoteDescription(new SkeletalSdpObserver() {
@Override
public void onSetSuccess() {
// createAnswer
peerConnection.createAnswer(new SkeletalSdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
peerConnection.setLocalDescription(new SkeletalSdpObserver() {}, sessionDescription);
}
}, WebRTCUtil.answerConnectionConstraints());
}
}, remoteDescription);
}
private void _createOffer(){
peerConnection.createOffer(new SkeletalSdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
peerConnection.setLocalDescription(new SkeletalSdpObserver() {}, sessionDescription);
}
}, WebRTCUtil.offerConnectionConstraints());
}
private void setupLocalStream() {
localStream = factory.createLocalMediaStream("android_local_stream");
// videoTrack
videoCapturer = createCameraCapturer(new Camera2Enumerator(activity));
VideoSource localVideoSource = factory.createVideoSource(videoCapturer);
localVideoTrack = factory.createVideoTrack("android_local_videotrack", localVideoSource);
localStream.addTrack(localVideoTrack);
// render
localRenderer = setupRenderer(R.id.local_render_view);
localVideoTrack.addRenderer(localRenderer);
// audioTrack
AudioSource audioSource = factory.createAudioSource(WebRTCUtil.mediaStreamConstraints());
AudioTrack audioTrack = factory.createAudioTrack("android_local_audiotrack", audioSource);
localStream.addTrack(audioTrack);
}
private void _startCapture(){
DisplayMetrics displayMetrics = new DisplayMetrics();
WindowManager windowManager =
(WindowManager) activity.getApplication().getSystemService(Context.WINDOW_SERVICE);
windowManager.getDefaultDisplay().getRealMetrics(displayMetrics);
int videoWidth = displayMetrics.widthPixels;
int videoHeight = displayMetrics.heightPixels;
videoCapturer.startCapture(videoWidth, videoHeight, 30);
}
private VideoRenderer setupRenderer(int viewId){
SurfaceViewRenderer renderer = (SurfaceViewRenderer) activity.findViewById(viewId);
renderer.init(eglBase.getEglBaseContext(), null);
renderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
renderer.setZOrderMediaOverlay(true);
renderer.setEnableHardwareScaler(true);
return new VideoRenderer(renderer);
}
private VideoCapturer createCameraCapturer(CameraEnumerator enumerator) {
return createBackCameraCapturer(enumerator);
}
private VideoCapturer createBackCameraCapturer(CameraEnumerator enumerator) {
final String[] deviceNames = enumerator.getDeviceNames();
for (String deviceName : deviceNames) {
if (!enumerator.isFrontFacing(deviceName)) {
VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
if (videoCapturer != null) {
return videoCapturer;
}
}
}
return null;
}
// PeerConnection.Observer -----
@Override
public void onSignalingChange(PeerConnection.SignalingState signalingState) {}
@Override
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {}
@Override
public void onIceConnectionReceivingChange(boolean b) {}
@Override
public void onRemoveStream(MediaStream mediaStream) {}
@Override
public void onDataChannel(DataChannel dataChannel) {}
@Override
public void onRenegotiationNeeded() {}
@Override
public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {}
@Override
public void onIceCandidate(IceCandidate iceCandidate) {}
@Override
public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {}
@Override
public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
Log.d(TAG, "onIceGatheringChange");
if(iceGatheringState == PeerConnection.IceGatheringState.COMPLETE){
Log.d(TAG, "Complete");
SessionDescription localSdp = peerConnection.getLocalDescription();
callbacks.onCreateLocalSdp(localSdp.description);
}
}
@Override
public void onAddStream(MediaStream mediaStream) {
if (mediaStream.videoTracks.size() == 0){
return;
}
final VideoTrack remoteVideoTrack = mediaStream.videoTracks.getFirst();
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
remoteRenderer = setupRenderer(R.id.remote_render_view);
remoteVideoTrack.addRenderer(remoteRenderer);
callbacks.didReceiveRemoteStream();
}
});
}
}
Wamp
Wamp.java
package com.nakadoribooks.webrtcexample;
import android.app.Activity;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.concurrent.TimeUnit;
import rx.android.app.AppObservable;
import rx.functions.Action0;
import rx.functions.Action1;
import ws.wamp.jawampa.PubSubData;
import ws.wamp.jawampa.WampClient;
import ws.wamp.jawampa.WampClientBuilder;
import ws.wamp.jawampa.WampError;
/**
* Created by kawase on 2017/04/15.
*/
public class Wamp {
public static interface WampCallbacks{
void onConnected();
void onReceiveOffer(String sdp);
void onReceiveAnswer(String sdp);
}
private static final String TAG = "Wamp";
private static final String AnswerTopic = "com.nakadoribook.webrtc.answer";
private static final String OfferTopic = "com.nakadoribook.webrtc.offer";
private final Activity activity;
private WampClient wampClient;
private WampCallbacks callbacks;
Wamp(Activity activity){
this.activity = activity;
}
// interface -------
public void connect(WampCallbacks callbacks){
this.callbacks = callbacks;
_connect();
}
public void publishOffer(String sdp){
_publishOffer(sdp);
}
public void publishAnswer(String sdp){
_publishAnswer(sdp);
}
// implements --------
private void _connect(){
WampClientBuilder builder = new WampClientBuilder();
try {
// builder.withUri("ws://192.168.1.2:8000")
builder.withUri("wss://nakadoribooks-webrtc.herokuapp.com")
.withRealm("realm1")
.withInfiniteReconnects()
.withReconnectInterval(3, TimeUnit.SECONDS);
wampClient = builder.build();
} catch (WampError e) {
return;
}
AppObservable.bindActivity(activity, wampClient.statusChanged())
.subscribe(new Action1<WampClient.Status>() {
@Override
public void call(final WampClient.Status status) {
if (status == WampClient.Status.Connected) {
callbacks.onConnected();
wampClient.makeSubscription(OfferTopic).subscribe(new Action1<PubSubData>(){
@Override
public void call(PubSubData arg0) {
JsonNode json = arg0.arguments().get(0);
String sdp = json.get("sdp").asText();
callbacks.onReceiveOffer(sdp);
}
}, new Action1<Throwable>(){
@Override
public void call(Throwable arg0) {}
});
wampClient.makeSubscription(AnswerTopic).subscribe(new Action1<PubSubData>(){
@Override
public void call(PubSubData arg0) {
JsonNode json = arg0.arguments().get(0);
String sdp = json.get("sdp").asText();
callbacks.onReceiveAnswer(sdp);
}
}, new Action1<Throwable>(){
@Override
public void call(Throwable arg0) {}
});
}
}
}, new Action1<Throwable>() {
@Override
public void call(final Throwable t) {}
}, new Action0() {
@Override
public void call() {}
});
wampClient.open();
}
public void _publishOffer(String sdp){
final ObjectMapper mapper = new ObjectMapper();
ObjectNode node = mapper.createObjectNode();
node.put("type", "offer");
node.put("sdp", sdp);
wampClient.publish(OfferTopic, node);
}
public void _publishAnswer(String sdp){
final ObjectMapper mapper = new ObjectMapper();
ObjectNode node = mapper.createObjectNode();
node.put("type", "answer");
node.put("sdp", sdp);
wampClient.publish(AnswerTopic, node);
}
}
アプリケーション
MainActivity.java
package com.nakadoribooks.webrtcexample;
import android.Manifest;
import android.graphics.Color;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
private enum State{
Disconnected
, Connecting
, Connected
, Offering
, ReceivedOffer
, CreatingAnswer
, Done
}
private static final String TAG = "MainActivity";
private static final int REQUEST_CODE_CAMERA_PERMISSION = 1;
private WebRTC webRTC;
private Wamp wamp;
private State state = State.Disconnected;
private Button controlButton;
private TextView statusText;
private boolean typeOffer = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// getView
controlButton = (Button) findViewById(R.id.control_button);
statusText = (TextView) findViewById(R.id.status_label);
// registerEvent
findViewById(R.id.control_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
onTapButton();
}
});
// wamp
wamp = new Wamp(this);
// checkPermission → onRequestPermissionsResult → startWebRTC
checkPermission();
}
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
if (requestCode != REQUEST_CODE_CAMERA_PERMISSION)
return;
startWebRTC();
}
private void checkPermission(){
String[] permissioins = new String[]{ Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO};
ActivityCompat.requestPermissions(this, permissioins, REQUEST_CODE_CAMERA_PERMISSION);
}
private void onTapButton(){
if(state == State.Disconnected){
changeState(State.Connecting);
connect();
}else if(state == State.Connected){
typeOffer = true;
changeState(State.Offering);
stateOffering();
webRTC.createOffer();
}
}
private void connect(){
wamp.connect(new Wamp.WampCallbacks() {
@Override
public void onConnected() {
changeState(State.Connected);
}
@Override
public void onReceiveOffer(String sdp) {
if(typeOffer){
return;
}
changeState(State.CreatingAnswer);
webRTC.receiveOffer(sdp);
}
@Override
public void onReceiveAnswer(String sdp) {
if(!typeOffer){
return;
}
webRTC.receiveAnswer(sdp);
}
});
}
private void startWebRTC(){
webRTC = new WebRTC(this);
webRTC.connect(new WebRTC.WebRTCCallbacks() {
@Override
public void onCreateLocalSdp(String sdp) {
if(typeOffer){
wamp.publishOffer(sdp);
}else{
wamp.publishAnswer(sdp);
}
}
@Override
public void didReceiveRemoteStream() {
changeState(State.Done);
}
});
webRTC.startCapture();
}
// view ---------------
private void stateConnecting(){
statusText.setText("Connecting");
controlButton.setText("Connecting...");
controlButton.setEnabled(false);
}
private void stateConnected(){
statusText.setText("connected");
statusText.setTextColor(Color.BLUE);
controlButton.setText("Send Offer");
controlButton.setEnabled(true);
}
private void stateOffering(){
statusText.setText("Offering...");
controlButton.setText("Offering...");
controlButton.setEnabled(false);
}
private void stateReceivedOffer(){
statusText.setText("ReceivedOffer");
controlButton.setText("ReceivedOffer");
controlButton.setEnabled(false);
}
private void stateCreatingAnswer(){
statusText.setText("CreatingAnswer...");
controlButton.setText("CreatingAnswer...");
controlButton.setEnabled(false);
}
private void stateDone(){
statusText.setText("OK!");
controlButton.setText("OK!");
controlButton.setEnabled(false);
}
private void changeState(final State state){
this.state = state;
runOnUiThread(new Runnable() {
@Override
public void run() {
switch (state){
case Connected:
stateConnected();
break;
case Connecting:
stateConnecting();
break;
case CreatingAnswer:
stateCreatingAnswer();
break;
case ReceivedOffer:
stateReceivedOffer();
break;
case Done:
stateDone();
default:
break;
}
}
});
}
}
美酒の設計
秋田の地酒 雪の茅舎美酒の設計純米吟醸火入れタイプ1,8L 齋弥酒造
http://www.sudasaketen.com/item/k0003/
美酒の設計 単行本 – 2009/11/26 藤田 千恵子 (著)
http://amzn.asia/efouKSJ
次
Trickle ICE を予定