一、技术背景
因业务需求需要基于sipserver实现音视频对讲,开源代码linephone即可实现。文末附Demo实现及源码,对讲的实现也基于服务端实现后,提供ip地址才行。
开源库地址:https://github.com/topics/linphone
二、基于安卓客户端实现
添加build.gradle依赖
repositories {
maven {
// Replace snapshots by releases for releases !
url "http://linphone.org/releases/maven_repository/"
}
}
implementation "org.linphone:linphone-sdk-android:4.3.1"
主要代码示例
package org.linphone.sample;
import android.app.Service;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.widget.Toast;
import androidx.annotation.Nullable;
import org.linphone.core.Call;
import org.linphone.core.CallParams;
import org.linphone.core.Core;
import org.linphone.core.CoreListenerStub;
import org.linphone.core.Factory;
import org.linphone.core.LogCollectionState;
import org.linphone.core.tools.Log;
import org.linphone.mediastream.Version;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Timer;
import java.util.TimerTask;
public class LinphoneService extends Service {
private static final String START_LINPHONE_LOGS = " ==== Device information dump ====";
// Keep a static reference to the Service so we can access it from anywhere in the app
private static LinphoneService sInstance;
private Handler mHandler;
private Timer mTimer;
private Core mCore;
private CoreListenerStub mCoreListener;
public static boolean isReady() {
return sInstance != null;
}
public static LinphoneService getInstance() {
return sInstance;
}
public static Core getCore() {
return sInstance.mCore;
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
// The first call to liblinphone SDK MUST BE to a Factory method
// So let's enable the library debug logs & log collection
String basePath = getFilesDir().getAbsolutePath();
Factory.instance().setLogCollectionPath(basePath);
Factory.instance().enableLogCollection(LogCollectionState.Enabled);
Factory.instance().setDebugMode(true, getString(R.string.app_name));
// Dump some useful information about the device we're running on
Log.i(START_LINPHONE_LOGS);
dumpDeviceInformation();
dumpInstalledLinphoneInformation();
mHandler = new Handler();
// This will be our main Core listener, it will change activities depending on events
mCoreListener = new CoreListenerStub() {
@Override
public void onCallStateChanged(Core core, Call call, Call.State state, String message) {
Toast.makeText(LinphoneService.this, message, Toast.LENGTH_SHORT).show();
if (state == Call.State.IncomingReceived) {
Toast.makeText(LinphoneService.this, "Incoming call received, answering it automatically", Toast.LENGTH_LONG).show();
// For this sample we will automatically answer incoming calls
CallParams params = getCore().createCallParams(call);
params.enableVideo(true);
call.acceptWithParams(params);
} else if (state == Call.State.Connected) {
// This stats means the call has been established, let's start the call activity
Intent intent = new Intent(LinphoneService.this, CallActivity.class);
// As it is the Service that is starting the activity, we have to give this flag
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
}
};
try {
// Let's copy some RAW resources to the device
// The default config file must only be installed once (the first time)
copyIfNotExist(R.raw.linphonerc_default, basePath + "/.linphonerc");
// The factory config is used to override any other setting, let's copy it each time
copyFromPackage(R.raw.linphonerc_factory, "linphonerc");
} catch (IOException ioe) {
Log.e(ioe);
}
// Create the Core and add our listener
mCore = Factory.instance()
.createCore(basePath + "/.linphonerc", basePath + "/linphonerc", this);
mCore.addListener(mCoreListener);
// Core is ready to be configured
configureCore();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
// If our Service is already running, no need to continue
if (sInstance != null) {
return START_STICKY;
}
// Our Service has been started, we can keep our reference on it
// From now one the Launcher will be able to call onServiceReady()
sInstance = this;
// Core must be started after being created and configured
mCore.start();
// We also MUST call the iterate() method of the Core on a regular basis
TimerTask lTask =
new TimerTask() {
@Override
public void run() {
mHandler.post(
new Runnable() {
@Override
public void run() {
if (mCore != null) {
mCore.iterate();
}
}
});
}
};
mTimer = new Timer("Linphone scheduler");
mTimer.schedule(lTask, 0, 20);
return START_STICKY;
}
@Override
public void onDestroy() {
mCore.removeListener(mCoreListener);
mTimer.cancel();
mCore.stop();
// A stopped Core can be started again
// To ensure resources are freed, we must ensure it will be garbage collected
mCore = null;
// Don't forget to free the singleton as well
sInstance = null;
super.onDestroy();
}
@Override
public void onTaskRemoved(Intent rootIntent) {
// For this sample we will kill the Service at the same time we kill the app
stopSelf();
super.onTaskRemoved(rootIntent);
}
private void configureCore() {
// We will create a directory for user signed certificates if needed
String basePath = getFilesDir().getAbsolutePath();
String userCerts = basePath + "/user-certs";
File f = new File(userCerts);
if (!f.exists()) {
if (!f.mkdir()) {
Log.e(userCerts + " can't be created.");
}
}
mCore.setUserCertificatesPath(userCerts);
}
private void dumpDeviceInformation() {
StringBuilder sb = new StringBuilder();
sb.append("DEVICE=").append(Build.DEVICE).append("\n");
sb.append("MODEL=").append(Build.MODEL).append("\n");
sb.append("MANUFACTURER=").append(Build.MANUFACTURER).append("\n");
sb.append("SDK=").append(Build.VERSION.SDK_INT).append("\n");
sb.append("Supported ABIs=");
for (String abi : Version.getCpuAbis()) {
sb.append(abi).append(", ");
}
sb.append("\n");
Log.i(sb.toString());
}
private void dumpInstalledLinphoneInformation() {
PackageInfo info = null;
try {
info = getPackageManager().getPackageInfo(getPackageName(), 0);
} catch (PackageManager.NameNotFoundException nnfe) {
Log.e(nnfe);
}
if (info != null) {
Log.i(
"[Service] Linphone version is ",
info.versionName + " (" + info.versionCode + ")");
} else {
Log.i("[Service] Linphone version is unknown");
}
}
private void copyIfNotExist(int ressourceId, String target) throws IOException {
File lFileToCopy = new File(target);
if (!lFileToCopy.exists()) {
copyFromPackage(ressourceId, lFileToCopy.getName());
}
}
private void copyFromPackage(int ressourceId, String target) throws IOException {
FileOutputStream lOutputStream = openFileOutput(target, 0);
InputStream lInputStream = getResources().openRawResource(ressourceId);
int readByte;
byte[] buff = new byte[8048];
while ((readByte = lInputStream.read(buff)) != -1) {
lOutputStream.write(buff, 0, readByte);
}
lOutputStream.flush();
lOutputStream.close();
lInputStream.close();
}
}
CallActivity
package org.linphone.sample;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.view.TextureView;
import android.view.View;
import android.widget.RelativeLayout;
import androidx.annotation.Nullable;
import org.linphone.core.Call;
import org.linphone.core.Core;
import org.linphone.core.CoreListenerStub;
import org.linphone.core.VideoDefinition;
import org.linphone.core.tools.Log;
import org.linphone.mediastream.Version;
public class CallActivity extends Activity {
// We use 2 TextureView, one for remote video and one for local camera preview
private TextureView mVideoView;
private TextureView mCaptureView;
private CoreListenerStub mCoreListener;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.call);
mVideoView = findViewById(R.id.videoSurface);
mCaptureView = findViewById(R.id.videoCaptureSurface);
Core core = LinphoneService.getCore();
// We need to tell the core in which to display what
core.setNativeVideoWindowId(mVideoView);
core.setNativePreviewWindowId(mCaptureView);
// Listen for call state changes
mCoreListener = new CoreListenerStub() {
@Override
public void onCallStateChanged(Core core, Call call, Call.State state, String message) {
if (state == Call.State.End || state == Call.State.Released) {
// Once call is finished (end state), terminate the activity
// We also check for released state (called a few seconds later) just in case
// we missed the first one
finish();
}
}
};
findViewById(R.id.terminate_call).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Core core = LinphoneService.getCore();
if (core.getCallsNb() > 0) {
Call call = core.getCurrentCall();
if (call == null) {
// Current call can be null if paused for example
call = core.getCalls()[0];
}
call.terminate();
}
}
});
}
@Override
protected void onStart() {
super.onStart();
}
@Override
protected void onResume() {
super.onResume();
LinphoneService.getCore().addListener(mCoreListener);
resizePreview();
}
@Override
protected void onPause() {
LinphoneService.getCore().removeListener(mCoreListener);
super.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
}
@TargetApi(24)
@Override
public void onUserLeaveHint() {
// If the device supports Picture in Picture let's use it
boolean supportsPip =
getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
Log.i("[Call] Is picture in picture supported: " + supportsPip);
if (supportsPip && Version.sdkAboveOrEqual(24)) {
enterPictureInPictureMode();
}
}
@Override
public void onPictureInPictureModeChanged(
boolean isInPictureInPictureMode, Configuration newConfig) {
if (isInPictureInPictureMode) {
// Currently nothing to do has we only display video
// But if we had controls or other UI elements we should hide them
} else {
// If we did hide something, let's make them visible again
}
}
private void resizePreview() {
Core core = LinphoneService.getCore();
if (core.getCallsNb() > 0) {
Call call = core.getCurrentCall();
if (call == null) {
call = core.getCalls()[0];
}
if (call == null) return;
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
int screenHeight = metrics.heightPixels;
int maxHeight =
screenHeight / 4; // Let's take at most 1/4 of the screen for the camera preview
VideoDefinition videoSize =
call.getCurrentParams()
.getSentVideoDefinition(); // It already takes care of rotation
if (videoSize.getWidth() == 0 || videoSize.getHeight() == 0) {
Log.w(
"[Video] Couldn't get sent video definition, using default video definition");
videoSize = core.getPreferredVideoDefinition();
}
int width = videoSize.getWidth();
int height = videoSize.getHeight();
Log.d("[Video] Video height is " + height + ", width is " + width);
width = width * maxHeight / height;
height = maxHeight;
if (mCaptureView == null) {
Log.e("[Video] mCaptureView is null !");
return;
}
RelativeLayout.LayoutParams newLp = new RelativeLayout.LayoutParams(width, height);
newLp.addRule(
RelativeLayout.ALIGN_PARENT_BOTTOM,
1); // Clears the rule, as there is no removeRule until API 17.
newLp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, 1);
mCaptureView.setLayoutParams(newLp);
Log.d("[Video] Video preview size set to " + width + "x" + height);
}
}
}
基于TextureView进行视频流渲染