Android端简易音视频通话
在编码和推流,有两个方案选择:一: 使用javacv来实现,最终也是用过ffmpeg来进行编码和推流,javacv实现到可以直接接收摄像头的帧数据需要自己实现的代码只是打开摄像头,写一个SurfaceView进行预览,然后实现PreviewCallback将摄像头每一帧的数据交给javacv即可javacv地址:https://github.com/bytedeco/javacvdemo地址:ht
前两天刚搭建了SRS服务器,正好利用SRS服务器搭建一个音视频通话的APP小demo玩玩,经过了解Android端推流&拉流后成功做出一个比较low的demo,不嫌弃的话可以看一看
在编码和推流,有两个方案选择:
一: 使用javacv来实现,最终也是用过ffmpeg来进行编码和推流,javacv实现到可以直接接收摄像头的帧数据
需要自己实现的代码只是打开摄像头,写一个SurfaceView进行预览,然后实现PreviewCallback将摄像头每一帧的数据交给javacv即可
javacv地址:https://github.com/bytedeco/javacv
demo地址:https://github.com/beautifulSoup/RtmpRecoder/tree/master
二:使用Android自带的编码工具,可实现硬编码,这里有一个国内大神开源的封装很完善的的库yasea,第一种方法需要实现的Camera采集部分也一起封装好了,进行一些简单配置就可以实现编码推流,并且yasea目前已经直接支持摄像头的热切换,和各种滤镜效果
yasea地址(内置demo):https://github.com/begeekmyfriend/yasea
服务器:
流媒体服务器我用的是srs,地址:https://github.com/ossrs/srs/wiki/v3_CN_Home
关于srs的编译、配置、部署、在官方wiki中已经写的很详细了,并且srs同样是国内开发人员开源的项目,有全中文的文档,看起来很方便
这里有最基本的简单编译部署过程 CentOS 7 JavaWeb 环境下SRS+Nginx搭建流媒体服务器
播放器:
android端的播放使用哔哩哔哩开源的ijkplayer播放器,vitamio播放器也可以
ijkplayer地址:https://github.com/Doikki/DKVideoPlayer
vitamio地址(内置demo):https://github.com/yixia/VitamioBundle
准备开源库:
直播实现的流程:
- 使用yaesa进行摄像头采集、编码然后向srs服务器rtmp推流
- 部署srs流媒体服务器
- 使用ijkplayer取流播放
demo很简单这里就不放了,可以看一下下面的Java文件和xml布局文件,布局很low,毕竟是简单实现,嫌弃就不要看了
具体实现:
MainActivity.java
public class MainActivity extends Activity implements SrsEncodeHandler.SrsEncodeListener, RtmpHandler.RtmpListener, SrsRecordHandler.SrsRecordListener, View.OnClickListener {
//拉流地址
private static final String URL_VOD = "rtmp://123.57.108.220/live/zhibo/zhibo/1";
//推流地址
private static final String rtmpUrl = "rtmp://123.57.108.220:1935/live/zhibo/0";
@BindView(R.id.player)
VideoView mPlayer;
@BindView(R.id.publish)
Button mPublish;
private SrsPublisher mYaseaCamera;
/**
* 所需的所有权限信息
*/
private static final String[] NEEDED_PERMISSIONS = new String[]{
Manifest.permission.INTERNET,
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
private static final int ACTION_REQUEST_PERMISSIONS = 0x001;
@SuppressLint("InvalidWakeLockTag")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
if (getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
//屏幕保持常亮
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
//权限检查
if (!checkPermissions(NEEDED_PERMISSIONS)) {
ActivityCompat.requestPermissions(MainActivity.this, NEEDED_PERMISSIONS, ACTION_REQUEST_PERMISSIONS);
}
//播放视频的方法
init();
}
/**
* 播放视频
*/
private void init() {
// init player
IjkMediaPlayer.loadLibrariesOnce(null);
// 没有太大用处
IjkMediaPlayer.native_profileBegin("libijkplayer.so");
//设置视频地址
mPlayer.setUrl(URL_VOD);
//设置控制器
// StandardVideoController controller = new StandardVideoController(this);
// controller.addDefaultControlComponent("视频频道", false);
// mPlayer.setVideoController(controller);
//这里不设置控制器
mPlayer.setVideoController(null);
//进入全屏
// mPlayer.startFullScreen();
//视频画面比例,这里使用的填充视频框模式,但是画面可能变形
mPlayer.setScreenScaleType(SCREEN_SCALE_MATCH_PARENT);
//设置解码模式,这里设置的IjkPlayer解码
mPlayer.setPlayerFactory(IjkPlayerFactory.create());
//使用IjkPlayer解码
// mPlayer.setPlayerFactory(IjkPlayerFactory.create());
//使用ExoPlayer解码
// mPlayer.setPlayerFactory(ExoMediaPlayerFactory.create());
//使用MediaPlayer解码
// mPlayer.setPlayerFactory(AndroidMediaPlayerFactory.create());
//开始播放,不调用则不自动播放
mPlayer.start();
camera();
}
/**
* 暂停播放
*/
@Override
protected void onPause() {
super.onPause();
mPlayer.pause();
mYaseaCamera.pauseRecord();
}
/**
* 继续播放
*/
@Override
protected void onResume() {
super.onResume();
mPlayer.resume();
mYaseaCamera.resumeRecord();
}
/**
* 释放播放器
*/
@Override
protected void onDestroy() {
super.onDestroy();
mYaseaCamera.stopPublish();
mYaseaCamera.stopRecord();
mPlayer.release();
}
/**
* 按下back键
*/
@Override
public void onBackPressed() {
if (!mPlayer.onBackPressed()) {
super.onBackPressed();
}
}
private void camera() {
mYaseaCamera = new SrsPublisher((SrsCameraView) findViewById(R.id.yasea_camera));
//编码状态回调
mYaseaCamera.setEncodeHandler(new SrsEncodeHandler(this));
mYaseaCamera.setRecordHandler(new SrsRecordHandler(this));
//rtmp推流状态回调
mYaseaCamera.setRtmpHandler(new RtmpHandler(this));
//预览分辨率
mYaseaCamera.setPreviewResolution(1280, 720);
//推流分辨率
mYaseaCamera.setOutputResolution(720, 1280);
//传输率
mYaseaCamera.setVideoHDMode();
//开启美颜(其他滤镜效果在MagicFilterType中查看)
mYaseaCamera.switchCameraFilter(MagicFilterType.BEAUTY);
//打开摄像头,开始预览(未推流)
mYaseaCamera.startCamera();
//硬编码
mYaseaCamera.switchToSoftEncoder();
//软编码
// mYaseaCamera.switchToHardEncoder();
mPublish.setOnClickListener(this);
}
@OnClick(R.id.publish)
public void onClick(View v) {
switch (v.getId()) {
default:
break;
case R.id.publish:
if (mPublish.getText().toString().contentEquals("开始")) {
//开始推流
mYaseaCamera.startPublish(rtmpUrl);
mYaseaCamera.startCamera();
mPublish.setText("停止");
mPublish.setVisibility(View.GONE);
}else {
mYaseaCamera.stopPublish();
mPublish.setText("开始");
}
break;
}
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mYaseaCamera.stopEncode();
mYaseaCamera.stopRecord();
mYaseaCamera.setScreenOrientation(newConfig.orientation);
if (mPublish.getText().toString().contentEquals("停止")) {
mYaseaCamera.startEncode();
}
mYaseaCamera.startCamera();
}
@Override
public void onRtmpConnecting(String msg) {
Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
}
@Override
public void onRtmpConnected(String msg) {
Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
}
@Override
public void onRtmpVideoStreaming() {
}
@Override
public void onRtmpAudioStreaming() {
}
@Override
public void onRtmpStopped() {
Toast.makeText(getApplicationContext(), "已停止", Toast.LENGTH_SHORT).show();
}
@Override
public void onRtmpDisconnected() {
Toast.makeText(getApplicationContext(), "未连接服务器", Toast.LENGTH_SHORT).show();
}
@Override
public void onRtmpVideoFpsChanged(double fps) {
}
@Override
public void onRtmpVideoBitrateChanged(double bitrate) {
}
@Override
public void onRtmpAudioBitrateChanged(double bitrate) {
}
@Override
public void onRtmpSocketException(SocketException e) {
handleException(e);
}
@Override
public void onRtmpIOException(IOException e) {
handleException(e);
}
@Override
public void onRtmpIllegalArgumentException(IllegalArgumentException e) {
handleException(e);
}
@Override
public void onRtmpIllegalStateException(IllegalStateException e) {
handleException(e);
}
@Override
public void onNetworkWeak() {
Toast.makeText(getApplicationContext(), "网络信号弱", Toast.LENGTH_SHORT).show();
}
@Override
public void onNetworkResume() {
}
@Override
public void onEncodeIllegalArgumentException(IllegalArgumentException e) {
handleException(e);
}
private void handleException(Exception e) {
try {
Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_SHORT).show();
mYaseaCamera.stopPublish();
mYaseaCamera.stopRecord();
mPublish.setText("开始");
} catch (Exception e1) {
//
}
}
@Override
public void onRecordPause() {
Toast.makeText(getApplicationContext(), "Record paused", Toast.LENGTH_SHORT).show();
}
@Override
public void onRecordResume() {
Toast.makeText(getApplicationContext(), "Record resumed", Toast.LENGTH_SHORT).show();
}
@Override
public void onRecordStarted(String msg) {
Toast.makeText(getApplicationContext(), "Recording file: " + msg, Toast.LENGTH_SHORT).show();
}
@Override
public void onRecordFinished(String msg) {
Toast.makeText(getApplicationContext(), "MP4 file saved: " + msg, Toast.LENGTH_SHORT).show();
}
@Override
public void onRecordIllegalArgumentException(IllegalArgumentException e) {
handleException(e);
}
@Override
public void onRecordIOException(IOException e) {
handleException(e);
}
/**
* 权限检查
*
* @param neededPermissions 需要的权限
* @return 是否全部被允许
*/
protected boolean checkPermissions(String[] neededPermissions) {
if (neededPermissions == null || neededPermissions.length == 0) {
return true;
}
boolean allGranted = true;
for (String neededPermission : neededPermissions) {
allGranted &= ContextCompat.checkSelfPermission(this, neededPermission) == PackageManager.PERMISSION_GRANTED;
}
return allGranted;
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.dueeeke.videoplayer.player.VideoView
android:id="@+id/player"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="MissingConstraints" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:layout_marginTop="20dp"
android:layout_marginRight="20dp"
android:orientation="vertical">
<net.ossrs.yasea.SrsCameraView
android:id="@+id/yasea_camera"
android:layout_width ="200dp"
android:layout_height="200dp"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center">
<Button
android:id="@+id/publish"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/dkplayer_theme_color"
android:focusable="true"
android:focusableInTouchMode="true"
android:textColor="#fff"
android:textSize="25sp"
android:text="开始"/>
</LinearLayout>
</FrameLayout>
</FrameLayout>
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)