通过上一篇的学习实践,我们了解了ExoPlayer的优缺点以及基本用法,今天我们进入ExoPlayer的音频播放实践,我们来一起实现一个简单的音频播放器。

目录

  1. 媒体播放框架MediaSession

  2. MediaSession框架+ExoPlayer 简单音乐播放器实践

  • 播放网络音乐

  • 播放/暂停

  • 歌曲切换

  • 倍速播放

一、媒体播放框架MediaSession

音频播放器并不总是需要使其UI可见。一旦开始播放音频,播放器就可以作为后台任务运行。用户可以切换到另一个应用程序,并继续听。
要在Android中实现这一设计,您可以使用两个组件构建一个音频应用程序:activity(展示所用) 和播放器service。如果用户切换到另一个应用程序,则该service可以在后台运行。通过将音频应用程序的两个部分分解为单独的组件,每个组件可以独立运行。与播放器相比,UI通常是短暂的,可能会在没有UI的情况下运行很长时间。

在设计音乐播放器APP架构时,有几种常用的做法

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

方案一

  1. 注册Service,用于数据设置、音乐控制,在Service中自定义播放器的一些状态值和回调接口用于流程控制

  2. 通过广播、aidl等实现和页面层逻辑的通信,使得用户可以通过界面控制音乐的播放、暂停、切换、seek等操作

  3. 使用RemoteControlClient(低版本)或者MediaSession(>5.0或者MediaSessionCompat)进行多端设备或者跨APP媒体会话

方案二
Android5.0时推出的MediaSession框架(Supprot包中MediaSessionCompat也对低版本做了支持),专门用来解决媒体播放时界面和Service通信的问题,在结构低耦合方面的设计做的比较好

支持库提供了两个类来实现此客户端/服务器方法:MediaBrowserService和MediaBrowser。该服务组件被实现为包含媒体会话及其播放器的MediaBrowserService的子类。使用UI和媒体控制器的活动应包括与MediaBrowserService进行通信的MediaBrowser。
使用MediaBrowserService可以让随身设备(如Android Auto and Wear)轻松发现您的应用,连接到它,浏览内容和控制播放,而无需访问您的Activity

我们今天的学习实践是基于方案二的MediaSession的框架

MediaBrowser
用来连接MediaBrowserService和订阅数据,通过他的回调可以获取和Service的连接状态以及获取在Service中异步获取的音乐数据(这个一般不在Service中进行获取,因为涉及到的是具体的业务逻辑)

MediaBrowserService
是一个Service,封装了媒体相关的一些功能,通过onGetRoot的返回值决定是否允许客户端连接。onLoadChildren回调在Sercive中异步获取的数据给到MediaBrowser。也包含媒体播放器实例(比如我们本篇实践的ExoPlayer)

MediaSession
一般在MediaBrowserService的onCreate中创建,通过MediaSession.CallBack回调接收MediaController发来的指令,触发对应的播放器相关的操作

MediaController
MediaContoller的创建需要MediaSession的配对令牌,在MediaBrowser连接服务成功之后创建。MediaController可以主动的发送指令或者被动的接收MediaController.Callback回调来改变播放状态和界面刷新。

更详细的介绍请参考官方文档或者Android 媒体播放框架MediaSession分析与实践

二、 简单实践

下面我们看下如何使用MediaSession框架实现简单的音频播放

2.1 Server端实现

首先我们继承MediaBrowserServiceCompat实现和注册Service

public class MusicService extends MediaBrowserServiceCompat {private static final String TAG = "MusicService";private SimpleExoPlayer exoPlayer;private MediaSessionCompat mediaSession;/*** 当服务收到onCreate()生命周期回调方法时,它应该执行以下步骤:* 1. 创建并初始化media session* 2. 设置media session回调* 3. 设置media session token*/@Overridepublic void onCreate() {Log.i(TAG, "onCreate: ");super.onCreate();//1. 创建并初始化MediaSessionmediaSession = new MediaSessionCompat(getApplicationContext(), TAG);mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS| MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder().setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PLAY_PAUSE| PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_PLAY_PAUSE |PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID |PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH |PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS |PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SEEK_TO).build();mediaSession.setPlaybackState(playbackState);//2. 设置mediaSession回调mediaSession.setCallback(new MyMediaSessionCallBack());//3. 设置mediaSessionTokensetSessionToken(mediaSession.getSessionToken());//创建播放器实例exoPlayer = new SimpleExoPlayer.Builder(getApplicationContext()).build();}
}

MediaSessionCompat.Callback的回调用于接收业务层通过mediaController.getTransportControls进行播放相关操作(播放、暂停、seek、倍速等等)的回调

 /*** 用于接收由MediaControl触发的改变,内部封装实现播放器和播放状态的改变*/private class MyMediaSessionCallBack extends MediaSessionCompat.Callback {@Overridepublic void onPlay() {super.onPlay();Log.i(TAG, "onPlay: ");exoPlayer.play();}@Overridepublic void onPause() {super.onPause();Log.i(TAG, "onPause: ");exoPlayer.pause();}@Overridepublic void onSeekTo(long pos) {super.onSeekTo(pos);Log.i(TAG, "onSeekTo: pos=" + pos);exoPlayer.seekTo(pos);}...}

MediaBrowserServiceCompat有两个回调方法onGetRoot和onLoadChildren。其中onGetRoot用于告诉MediaBrowser是否连接连接成功;onLoadChildren则是加载音视频数据。
具体使用如下:

 @Nullable@Overridepublic BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) {Log.i(TAG, "onGetRoot: clientPackageName=" + clientPackageName + " clientUid=" + clientUid + " pid=" + Binder.getCallingPid()+ " uid=" + Binder.getCallingUid());//返回非空,表示连接成功return new BrowserRoot("media_root_id", null);}//获取音视频信息(这个更应该是在业务层处理事情)@Overridepublic void onLoadChildren(@NonNull String parentId, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {Log.i(TAG, "onLoadChildren: parentId=" + parentId);List<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>();if (TextUtils.equals("media_root_id", parentId)) {}ArrayList<MusicEntity> musicEntityList = getMusicEntityList();for (int i = 0; i < musicEntityList.size(); i++) {MusicEntity musicEntity = musicEntityList.get(i);MediaMetadataCompat metadataCompat = buildMediaMetadata(musicEntity);if (i == 0) {mediaSession.setMetadata(metadataCompat);}mediaItems.add(new MediaBrowserCompat.MediaItem(metadataCompat.getDescription(), MediaBrowserCompat.MediaItem.FLAG_BROWSABLE));exoPlayer.addMediaItem(MediaItem.fromUri(musicEntity.source));}//当设置多首歌曲组成队列时报错// IllegalStateException: sendResult() called when either sendResult() or sendError() had already been called for: media_root_id//原因,之前在for处理了,应该在设置好mediaItems列表后,统一设置resultresult.sendResult(mediaItems);Log.i(TAG, "onLoadChildren: addMediaItem");initExoPlayerListener();exoPlayer.prepare();Log.i(TAG, "onLoadChildren: prepare");}private void initExoPlayerListener() {exoPlayer.addListener(new Player.EventListener() {@Overridepublic void onPlaybackStateChanged(int state) {long currentPosition = exoPlayer.getCurrentPosition();long duration = exoPlayer.getDuration();//状态改变(播放器内部发生状态变化的回调,// 包括// 1. 用户触发的  比如:手动切歌曲、暂停、播放、seek等;// 2. 播放器内部触发 比如:播放结束、自动切歌曲等)//该如何通知给ui业务层呐??好些只能通过回调//那有该如何 --》查看源码得知通过setPlaybackState设置Log.i(TAG, "onPlaybackStateChanged: currentPosition=" + currentPosition + " duration=" + duration + " state=" + state);int playbackState;switch (state) {default:case Player.STATE_IDLE:playbackState = PlaybackStateCompat.STATE_NONE;break;case Player.STATE_BUFFERING:playbackState = PlaybackStateCompat.STATE_BUFFERING;break;case Player.STATE_READY:if(exoPlayer.getPlayWhenReady()){playbackState = PlaybackStateCompat.STATE_PLAYING;}else {playbackState = PlaybackStateCompat.STATE_PAUSED;}break;case Player.STATE_ENDED:playbackState = PlaybackStateCompat.STATE_STOPPED;break;}//播放器的状态变化,通过mediasession告诉在ui业务层注册的MediaControllerCompat.Callback进行回调setPlaybackState(playbackState);}private void setPlaybackState(int playbackState) {float speed = exoPlayer.getPlaybackParameters() == null ? 1f : exoPlayer.getPlaybackParameters().speed;mediaSession.setPlaybackState(new PlaybackStateCompat.Builder().setState(playbackState, exoPlayer.getCurrentPosition(), speed).build());}@NotNullprivate ArrayList<MusicEntity> getMusicEntityList() {ArrayList<MusicEntity> list = new ArrayList<MusicEntity>();...MusicEntity musicEntity2 = new MusicEntity();musicEntity2.id = "wake_up_02";musicEntity2.title = "Geisha";musicEntity2.album = "Wake Up";musicEntity2.artist = "Media Right Productions";musicEntity2.genre = "Electronic";musicEntity2.source = "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/02_-_Geisha.mp3";musicEntity2.image = "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg";musicEntity2.trackNumber = 2;musicEntity2.totalTrackCount = 13;musicEntity2.duration = 267;musicEntity2.site = "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/";list.add(musicEntity2);return list;}

2.2 Client端实现

下面我们再来看下Client端的实现

public class ExoSimpleAudioPlayerActivity extends Activity implements View.OnClickListener {private MediaBrowserCompat mediaBrowser;private MediaBrowserCompat.ConnectionCallback mConnectionCallbacks = new MyConnectionCallback();private MediaControllerCompat.Callback mMediaControllerCallback;private MediaBrowserCompat.SubscriptionCallback mSubscriptionCallback;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_simple_audio);
...//mConnectionCallbacks 是C-S连接的callbackmediaBrowser = new MediaBrowserCompat(this, new ComponentName(this, MusicService.class),mConnectionCallbacks, null);}@Overrideprotected void onStart() {super.onStart();Log.i(TAG, "onStart: ");//发出C-S连接请求 创建MusicService,收到onGetRoot回调值不为空说明建立连接成功--》然后触发MyConnectionCallback的回调onConnectedmediaBrowser.connect();
//        subscribe();}@Overrideprotected void onStop() {super.onStop();Log.i(TAG, "onStop: ");mediaBrowser.disconnect();}
}

MediaBrowserCompat.ConnectionCallback用于接收与Server端连接的状态回调

    public class MyConnectionCallback extends MediaBrowserCompat.ConnectionCallback {@Overridepublic void onConnected() {super.onConnected();Log.i(TAG, "onConnected: MyConnectionCallback");//MediaBrowser和MediaBrowerService建立连接之后会回调该方法MediaSessionCompat.Token sessionToken = mediaBrowser.getSessionToken();//建立连接之后再创建MediaControllermediaController = new MediaControllerCompat(ExoSimpleAudioPlayerActivity.this, sessionToken);MediaControllerCompat.setMediaController(ExoSimpleAudioPlayerActivity.this, mediaController);subscribe();//MediaController发送命令buildTransportControls();if (mMediaControllerCallback == null) {//这个callback 是Controller的callback,即用户触发了播放、暂停,后发生状态变化的回调。//像播放结束、自动切歌,则无法收到该回调(那该如何处理呐?)mMediaControllerCallback = new MediaControllerCompat.Callback() {//这里的回调,只有用户触发的才会有相应的回调。//播放结束 这里没有//ExoPlayer getDuration : https://stackoverflow.com/questions/35298125/exoplayer-getduration// Overridepublic void onPlaybackStateChanged(PlaybackStateCompat state) {super.onPlaybackStateChanged(state);Log.i(TAG, "onPlaybackStateChanged: state=" + state.getState());if (PlaybackStateCompat.STATE_PLAYING == state.getState()) {playButton.setText("暂停");} else {playButton.setText("播放");}updatePlaybackState(state);MediaMetadataCompat metadata = mediaController.getMetadata();updateDuration(metadata);}@Overridepublic void onMetadataChanged(MediaMetadataCompat metadata) {super.onMetadataChanged(metadata);durationSet = false;Log.i(TAG, "onMetadataChanged: metadata=" + metadata.toString());updateDuration(metadata);}}mediaController.registerCallback(mMediaControllerCallback);PlaybackStateCompat state = mediaController.getPlaybackState();updatePlaybackState(state);updateProgress();if (state != null && (state.getState() == PlaybackStateCompat.STATE_PLAYING ||state.getState() == PlaybackStateCompat.STATE_BUFFERING)) {scheduleSeekbarUpdate();}//通过mediaController获取MediaMetadataCompatMediaMetadataCompat metadata = mediaController.getMetadata();updateDuration(metadata);}@Overridepublic void onConnectionFailed() {super.onConnectionFailed();}}

2.3 基本功能

歌曲播放播放暂停
当用户点击了播放/暂停按钮后,获取当前的播放状态,通过mediaController.getTransportControls给到通过Binder给到mediaSession,在service中MediaSessionCompat.Callback改变Exoplayer的播放状态,exoplayer的onPlaybackStateChanged收到播放状态改变的通知后触发,给mediasession设置mediaSession.setPlaybackState

对应关键代码如下:

 client端用户点击事件处理//ExoSimpleAudioPlayerActivity.javaPlaybackStateCompat playbackState = mediaController.getPlaybackState();int state = playbackState.getState();Log.i(TAG, "onClick: state=" + state);//通过 mediaController.getTransportControls 触发MediaSessionCompat.Callback回调--》进行播放控制if (state == PlaybackStateCompat.STATE_PLAYING) {mediaController.getTransportControls().pause();} else {mediaController.getTransportControls().play();}

//Server端MediasessionCallback实现,接收mediaController.getTransportControls()的事件

//com.example.myplayer.audio.MusicService.MyMediaSessionCallBack@Overridepublic void onPlay() {super.onPlay();Log.i(TAG, "onPlay: ");exoPlayer.play();}@Overridepublic void onPause() {super.onPause();Log.i(TAG, "onPause: ");exoPlayer.pause();}

//server端 exoplayer状态变化监听

//com.example.myplayer.audio.MusicService#initExoPlayerListenerexoPlayer.addListener(new Player.EventListener() {@Overridepublic void onPlaybackStateChanged(int state) {long currentPosition = exoPlayer.getCurrentPosition();long duration = exoPlayer.getDuration();//状态改变(播放器内部发生状态变化的回调,// 包括// 1. 用户触发的  比如:手动切歌曲、暂停、播放、seek等;// 2. 播放器内部触发 比如:播放结束、自动切歌曲等)//该如何通知给ui业务层呐??好些只能通过回调//那有该如何 --》查看源码得知通过setPlaybackState设置Log.i(TAG, "onPlaybackStateChanged: currentPosition=" + currentPosition + " duration=" + duration + " state=" + state);int playbackState;switch (state) {default:case Player.STATE_IDLE:playbackState = PlaybackStateCompat.STATE_NONE;break;case Player.STATE_BUFFERING:playbackState = PlaybackStateCompat.STATE_BUFFERING;break;case Player.STATE_READY:if(exoPlayer.getPlayWhenReady()){playbackState = PlaybackStateCompat.STATE_PLAYING;}else {playbackState = PlaybackStateCompat.STATE_PAUSED;}break;case Player.STATE_ENDED:playbackState = PlaybackStateCompat.STATE_STOPPED;break;}//播放器的状态变化,通过mediasession告诉在ui业务层注册的MediaControllerCompat.Callback进行回调setPlaybackState(playbackState);}
}private void setPlaybackState(int playbackState) {float speed = exoPlayer.getPlaybackParameters() == null ? 1f : exoPlayer.getPlaybackParameters().speed;mediaSession.setPlaybackState(new PlaybackStateCompat.Builder().setState(playbackState, exoPlayer.getCurrentPosition(), speed).build());}

虽然知道了怎么使用,但是整个流程是怎样的呐?
其中用到了Handler和Binder的线程和进程通信相关的知识,后续我们专题单独深入学习实践下,这里我们先顺着流程画下播放/暂停的流程图,从用户按下按钮到播放器开始播放以及页面更新的整个流程是怎样的。

上一首下一首切换
歌曲切换流程个上面的播放流程基本上一致,

//com.example.myplayer.audio.ExoSimpleAudioPlayerActivity#onClickif (id == R.id.prev) {if (mediaController != null) {mediaController.getTransportControls().skipToPrevious();}} else if (id == R.id.next) {if (mediaController != null) {mediaController.getTransportControls().skipToNext();}}

区别在于 没有触发ExoPlayer的播放回调,需要再sessionCallback中调用exoplayer的next/prev进行歌曲切换,并且设置新的playstate状态给到mession

//com.example.myplayer.audio.MusicService.MyMediaSessionCallBack@Overridepublic void onSkipToNext() {super.onSkipToNext();Log.i(TAG, "onSkipToNext: ");exoPlayer.next();exoPlayer.setPlayWhenReady(true);setPlaybackState(PlaybackStateCompat.STATE_SKIPPING_TO_NEXT);mediaSession.setMetadata(getMediaMetadata(1));}@Overridepublic void onSkipToPrevious() {super.onSkipToPrevious();Log.i(TAG, "onSkipToPrevious: ");exoPlayer.previous();exoPlayer.setPlayWhenReady(true);setPlaybackState(PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS);mediaSession.setMetadata(getMediaMetadata(0));}

最终MediaControllerCallback的onPlaybackStateChanged收到回调,根据状态进行

   public void onPlaybackStateChanged(PlaybackStateCompat state) {super.onPlaybackStateChanged(state);...if (state.getState() == PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS || state.getState() == PlaybackStateCompat.STATE_SKIPPING_TO_NEXT) {updateShowMediaInfo(description);}
}private void updateShowMediaInfo(MediaDescriptionCompat description) {if (description == null) return;titleView.setText(description.getTitle());artistView.setText(description.getSubtitle());Glide.with(ExoSimpleAudioPlayerActivity.this).load(description.getIconUri().toString()).into(iconView);Uri mediaUri = description.getMediaUri();Uri iconUri = description.getIconUri();Log.i(TAG, "onChildrenLoaded: title=" + description.getTitle() + " subtitle=" + description.getSubtitle()+ " mediaUri=" + mediaUri + " iconUri=" + iconUri);}

倍速

//com.example.myplayer.audio.ExoSimpleAudioPlayerActivity#onClick
if (id == R.id.speed) {if (mediaController != null) {float speed = getSpeed();speedView.setText("倍速 " + speed);mediaController.getTransportControls().setPlaybackSpeed(speed);}}float[] speedArray = new float[]{0.5f, 1f, 1.5f, 2f};int curSpeedIndex = 1;private float getSpeed() {if (curSpeedIndex > 3) {curSpeedIndex = 0;}return speedArray[curSpeedIndex++];}

然后再MediaSessionCallBack中实现onSetPlaybackSpeed回调,进行播放倍速设置以及mession的设置

//com.example.myplayer.audio.MusicService.MyMediaSessionCallBack@Overridepublic void onSetPlaybackSpeed(float speed) {super.onSetPlaybackSpeed(speed);Log.i(TAG, "onSetPlaybackSpeed: speed=" + speed);PlaybackParameters playParams = new PlaybackParameters(speed);exoPlayer.setPlaybackParameters(playParams);//重新设置mediaSession.setPlaybackState 告知 监听者 speed变化setPlaybackState(exoPlayer.getPlaybackState());}private void setPlaybackState(int playbackState) {float speed = exoPlayer.getPlaybackParameters() == null ? 1f : exoPlayer.getPlaybackParameters().speed;mediaSession.setPlaybackState(new PlaybackStateCompat.Builder().setState(playbackState, exoPlayer.getCurrentPosition(), speed).build());}

需要注意
播放状态 MediaSession框架和ExoPlayer的不同与联系

//android.support.v4.media.session.PlaybackStateCompat
TATE_NONE, STATE_STOPPED, STATE_PAUSED, STATE_PLAYING, STATE_FAST_FORWARDING,STATE_REWINDING, STATE_BUFFERING, STATE_ERROR, STATE_CONNECTING,STATE_SKIPPING_TO_PREVIOUS, STATE_SKIPPING_TO_NEXT, STATE_SKIPPING_TO_QUEUE_ITEM

//com.google.android.exoplayer2.Player.State

STATE_IDLE, STATE_BUFFERING, STATE_READY, STATE_ENDED

其他

  1. android 禁用和开启四大组件的方法(setComponentEnabledSetting )

  2. Android 通知渠道Notification Channel

网络接口以及歌曲来源

来自google官方的uamp开源项目http://storage.googleapis.com/automotive-media/music.json
https://storage.googleapis.com/uamp/catalog.jsonMusic provided by the [Free Music Archive](http://freemusicarchive.org/).- [Irsen's Tale](http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/) by
[Kai Engel](http://freemusicarchive.org/music/Kai_Engel/).
- [Wake Up](http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/) by
[The Kyoto Connection](http://freemusicarchive.org/music/The_Kyoto_Connection/).长音频:https://v.typlog.com/oohomechat/8385162738_706123.mp3

收获

通过本篇的学习实践,

  1. 了解媒体播放框架MediaSession

  2. 使用MediaSession框架实现简单的音频播放器(播放/暂停、切歌、倍速)

  3. 了解原理、具体实践以及流程分析,我们基本了解MediaSession的框架以及ExoPlayer简单实用。
    但是一个音频播放器以下功能也是基本功能:边缓存变播放、播放队列、淡入淡出、音频焦点、后台播放,该如何比较好的实现呐?在具体实践之前我们先来学习分析下uamp这个google开源的音频播放器是如何架构的,看看在数据源设置以及播放管理方面是否可以学习借鉴。

感谢你的阅读

下一篇我们继续学习实践ExoPlayer,分析uamp的设计与实现

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

音视频开发(三十八):ExoPlayer 音频播放器实践相关推荐

  1. 即时通讯音视频开发(十八):详解音频编解码的原理、演进和应用选型

    1.引言 大家好,我是刘华平,从毕业到现在我一直在从事音视频领域相关工作,也有一些自己的创业项目,曾为早期Google Android SDK多媒体架构的构建作出贡献. 就音频而言,无论是算法多样性, ...

  2. 即时通讯音视频开发(十):实时语音通讯的回音消除技术详解

    前言 即时通讯应用中的实时音视频技术,几乎是IM开发中的最后一道高墙.原因在于:实时音视频技术 = 音视频处理技术 + 网络传输技术 的横向技术应用集合体,而公共互联网不是为了实时通信设计的.有关实时 ...

  3. 即时通讯音视频开发(十四):实时音视频数据传输协议介绍

    概述 随着移动互联网的快速发展以及智能终端性能的逐步提高,智能终端间进行实时音视频通讯成为移动互联网发展的一个重要方向.那么如何保证智能终端之间实时音视频数据通讯成为一个很现实的问题. 实际上,实时音 ...

  4. Android IOS WebRTC 音视频开发总结(八十七)-- WebRTC中丢包重传NACK实现分析

    Android IOS WebRTC 音视频开发总结(八十七)-- WebRTC中丢包重传NACK实现分析 本文主要介绍WebRTC中丢包重传NACK的实现,作者:weizhenwei ,文章最早发表 ...

  5. 音视频开发(四)——编码音频

    基于QT+FFMPEG的音视频开发(四)--编码音频 一.编码一般步骤 二.编码 2.1 创建编码器(本文创建AAC) 2.2 核心编码 三.源码 我的大部分学习都来自雷神,没有基础去雷神博客转转,每 ...

  6. Android 音视频开发(三) -- Camera2 实现预览、拍照功能

    音视频 系列文章 Android 音视频开发(一) – 使用AudioRecord 录制PCM(录音):AudioTrack播放音频 Android 音视频开发(二) – Camera1 实现预览.拍 ...

  7. Android音视频开发,详说PCM音频重采样、PCM编码

    直播伴音,两种数据能否合在一起?不能叠加在一起 会有噪音 合并以后 再去编码推流 直播的例子 客户端播放器,可以开启多个播放器 对于我们重采样 很多时候就是为了统一格式,就是为了要合并这个流,去推送, ...

  8. 音视频开发入门基础知识(音频入门篇)

    RTSP实时音视频开发实战课程:<RTSP实时音视频开发实战> 音视频开发入门基础知识(音频入门篇) 目录 前言 音频的采集和播放 音频常见的格式 音频的编码 前言 在音视频开发入门基础知 ...

  9. 即时通讯音视频开发(七):音频基础及编码原理入门

    前言 即时通讯应用中的实时音视频技术,几乎是IM开发中的最后一道高墙.原因在于:实时音视频技术 = 音视频处理技术 + 网络传输技术 的横向技术应用集合体,而公共互联网不是为了实时通信设计的. 系列文 ...

最新文章

  1. 开源电子工作套件 Arduino Start Kit 登场
  2. matlab i型级联filter,Matlab中filter,conv,impz用法(最新整理)
  3. 前端进阶-Event
  4. 关于 SAP Spartacus 的 featureModules
  5. CSV大文件的处理(以ngsim数据为例):分割、导入与合并
  6. MySQL -通过调整索引提升查询效率
  7. Drools 7.11 :入门程序
  8. cocos2d-x 3.2 项目开发 ValueMap 的遍历
  9. NNACL2021 放榜啦~
  10. paip.分布式应用系统java c#.net php的建设方案
  11. 我的世界自定义脚本生成器易语言源码
  12. 曲奇云盘资源搜索引擎_曲奇云盘下载-曲奇云盘官网版v3.2.4-sosyes
  13. 高德地图热力图,高德自带热力图heatmap
  14. 用 TypeScript 写一个轻量级的 UI 框架之八:表单控件之富文本编辑器
  15. 单细胞转录组测序建库方法小结
  16. 苦难是人生中必须经历的一课
  17. PostgreSQL安装(绿色版)
  18. idea svn IP地址更换方法
  19. 财富自由的声音:蚂蚁上市前,取消了周报
  20. 帝国 loginjs.php,帝国cms JS调用登陆模板制作教程

热门文章

  1. Stimulsoft Ultimate 2021.3.6 Crack
  2. Unity 基础 - 预设
  3. Win10:无法生成“D:\System Volume Information”下常规子目录的列表。拒绝访问。
  4. 你想学的都在这里!史上最全的《Android面试题及解析》,赶紧收藏!
  5. 网上投递简历的要诀!
  6. 赤峰公交出行-方便快捷
  7. 【英语流利说】数据分析师笔试+二面经验
  8. dell服务器恢复read信息,硬盘恢复的案例研究:戴尔错误代码0141
  9. Android 快速实现APK瘦身操作
  10. 部署JavaWeb项目到Linux 云服务器上