Android之媒体探究(一)—— 音频

在android 中随着手机性能的不断完善和提高,传统的图文慢慢转向了音视频,因为相对于音视频的信息传递,传统的图文就显得有些慢了,所以现在很多的APP里面或多或少都会包含一些音视频的功能在里面,所以对于开发者而言,学习和掌握音视频技术就显得比较重要了。废话不多说,直接进入正题。

音频部分分为两大块,一个是系统自带的MediaPlayer,一个是框架ExoPlayer。

系统自带的MediaPlayer

看过源码的同学都知道,MediaPlayer在应用层并没有做太多的事情,大部分的工作全部是交给Native层去实现的,也就是交给C/C++去实现的。而对于应用层来说,使用起来非常简单,直接调用系统的API就可以了,现在具体来看看。

第一步:创建MediaPlayer

创建MediaPlayer的方式有两种:

通过new的方式去获取MediaPlayer的实例
MediaPlayer mp = new MediaPlayer();

使用create的方式
MediaPlayer mp = MediaPlayer.create(this, R.raw.test);//这时就不用调用setDataSource

第二步:设置播放的音频文件

MediaPlayer要播放的文件主要包括3个来源:

a. 用户在应用中事先自带的resource资源
例如:MediaPlayer.create(this, R.raw.test);

b. 存储在SD卡或其他文件路径下的媒体文件
例如:mp.setDataSource("/sdcard/test.mp3");

c. 网络上的媒体文件
例如:mp.setDataSource("http://www.citynorth.cn/music/confucius.mp3");

在这里需要强调的是MediaPlayer的setDataSource的重载方法有很多,下面重点介绍以下几种:
(1)setDataSource (String path)
其中path可以是本地的音频文件的绝对路径也可以是网络路径

(2)setDataSource (Context context, Uri uri)
将本地的资源转换为Uri

(3)setDataSource (FileDescriptor fd, long offset, long length)
使用FileDescriptor时,需要将文件放到与res文件夹平级的assets文件夹里。

AssetFileDescriptor fileDescriptor = getAssets().openFd("rain.mp3");
mediaPlayer.setDataSource(fileDescriptor.getFileDescriptor(),fileDescriptor.getStartOffset(), fileDescriptor.getLength());
第三步:装载媒体资源

在设置好播放的媒体资源之后还需要做一个操作就是装载资源,也就是将流设置到MediaPlayer当中去,系统提供了两种装载资源的方法,即使用prepare()或prepareAsync()方法把流媒体装载进MediaPlayer,才可以调用start()方法播放流媒体。一般情况下我们为了防止堵塞主线程会采取异步装载的方式,即

mediaPlayer.prepareAsync()

第四步:执行播放操作

在装载结束后,可以直接调用start()方法来开始播放我们的音频文件了,那么这里就会有一个疑问,什么时候知道装载结束了呢?不要慌,系统为我们提供了监听的方法setOnPreparedListener

mediaPlayer?.setOnPreparedListener {
            mediaPlayer?.start()
}
至此,一个完整的播放流程就结束了,当然除此之外还有一些比较重要的知识点,如下所示。

知识点一:MediaPlayer的API

void start():开始或者恢复播放

void stop():停止播放

void pause():暂停播放

这3个方法是控制播放状态的,除此之外还有一些其他的API,如下所示

int getDuration():获取流媒体的总播放时长,单位是毫秒。
int getCurrentPosition():获取当前流媒体的播放的位置,单位是毫秒。
void seekTo(int msec):设置当前MediaPlayer的播放位置,单位是毫秒。
void setLooping(boolean looping):设置是否循环播放。
boolean isLooping():判断是否循环播放。
boolean isPlaying():判断是否正在播放。
void prepare():同步的方式装载流媒体文件。
void prepareAsync():异步的方式装载流媒体文件。
void release ():回收流媒体资源。
void setAudioStreamType(int streamtype):设置播放流媒体类型。
void setWakeMode(Context context, int mode):设置CPU唤醒的状态。

需要强调说明的是:
setAudioStreamType()方法用于指定播放流媒体的类型,它传递的是一个int类型的数据,均以常量定义在AudioManager类中, 一般我们播放音频文件,设置为AudioManager.STREAM_MUSIC即可

知识点二:MediaPlayer的回调
(1) setOnCompletionListener(MediaPlayer.OnCompletionListener listener):当流媒体播放完毕的时候回调。
(2) setOnErrorListener(MediaPlayer.OnErrorListener listener):当播放中发生错误的时候回调。
(3) setOnPreparedListener(MediaPlayer.OnPreparedListener listener):当装载流媒体完毕的时候回调。
(4) setOnSeekCompleteListener(MediaPlayer.OnSeekCompleteListener listener):当使用seekTo()设置播放位置的时候回调。

知识点三:MediaPlayer的回收
使用完MediaPlayer需要回收资源。MediaPlayer是很消耗系统资源的,所以在使用完MediaPlayer,不要等待系统自动回收,最好是主动回收资源。

if (mediaPlayer != null && mediaPlayer.isPlaying()) {
           mediaPlayer.stop();
           mediaPlayer.release();
           mediaPlayer = null;
}

知识点四:MediaPlayer的小例子

mediaPlayer.jpg

其实在实际开发中,我们应该将播放的逻辑与Service相结合,这里为了测试效果就没有写的那么完整

class MediaPlayerActivity : AppCompatActivity() {

    private var mediaPlayer: MediaPlayer? = null
    private var mList: MutableList<String> = mutableListOf()
    private var mCurrentPlayPosition = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initMusic()
        initMediaPlayer()
    }

    /**
     * 加载音频文件
     */
    private fun initMusic() {
        mList.add("https://xysx-voice.oss-cn-shanghai.aliyuncs.com/audio/2e76a133b898a2f6c8fb62e963b87ce4_tongkuercanlandeyisheng.mp3")
        mList.add("https://xysx-voice.oss-cn-shanghai.aliyuncs.com/audio/e845a564e795c628c6bdd652ddbc24e3_fulidadeqingshaonianshidai.mp3")
        mList.add("https://xysx-voice.oss-cn-shanghai.aliyuncs.com/audio/e9b9abdcc3855586ec2c4651293c11e9_abuyuwulei.mp3")
        mList.add("https://xysx-voice.oss-cn-shanghai.aliyuncs.com/audio/ba2eaf007614fdf81c8d9e895ba88ff2_abuchuangzuodekaishi.mp3")
    }

    /**
     * 初始化MediaPlayer
     */
    private fun initMediaPlayer() {
        // 1.实例化MediaPlayer
        // 也可以使用create的方式,如:
        // MediaPlayer mp = MediaPlayer.create(this, R.raw.test);//这时就不用调用setDataSource了
        mediaPlayer = MediaPlayer()
        // 2.设置播放的音频文件
        // 设置的音频文件的来源有3种
        // a. 用户在应用中事先自带的resource资源
        // 例如:MediaPlayer.create(this, R.raw.test);
        // b. 存储在SD卡或其他文件路径下的媒体文件,传入的是本地音频文件的绝对路径
        // 例如:mp.setDataSource("/sdcard/test.mp3");
        // c. 网络上的媒体文件,传入的是网络地址
        // 例如:mp.setDataSource("http://www.citynorth.cn/music/confucius.mp3");
        mediaPlayer?.setDataSource(mList[mCurrentPlayPosition])
        // MediaPlayer的setDataSource一共四个方法:
        // setDataSource (String path)
        // setDataSource (FileDescriptor fd)
        // setDataSource (Context context, Uri uri)
        // setDataSource (FileDescriptor fd, long offset, long length)
        // 其中使用FileDescriptor时,需要将文件放到与res文件夹平级的assets文件夹里,然后使用:
        // AssetFileDescriptor fileDescriptor = getAssets().openFd("rain.mp3");
        // mediaPlayer.setDataSource(fileDescriptor.getFileDescriptor(),fileDescriptor.getStartOffset(), fileDescriptor.getLength());来设置datasource
        // 3.装载我们的音频文件
        mediaPlayer?.prepareAsync()
        // 4.播放我们的音频文件
        mediaPlayer?.setOnPreparedListener {
            mediaPlayer?.start()
        }
    }

    /**
     * 上一首
     */
    fun lastMusic(view: View) {
        mediaPlayer?.reset()
        if (mCurrentPlayPosition == 0) {
            mCurrentPlayPosition = mList.size - 1
        } else {
            mCurrentPlayPosition--
        }
        mediaPlayer?.setDataSource(mList[mCurrentPlayPosition])
        mediaPlayer?.prepareAsync()
        mediaPlayer?.setOnPreparedListener {
            mediaPlayer?.start()
        }
    }

    /**
     * 播放
     */
    fun startMusic(view: View) {
        mediaPlayer?.start()
    }

    /**
     * 暂停
     */
    fun pauseMusic(view: View) {
        mediaPlayer?.pause()
    }

    /**
     * 结束
     */
    fun stopMusic(view: View) {
        mediaPlayer?.stop()
    }

    /**
     * 下一首
     */
    fun nextMusic(view: View) {
        try {
            mediaPlayer?.reset()
            if (mCurrentPlayPosition == mList.size - 1) {
                mCurrentPlayPosition = 0
            } else {
                mCurrentPlayPosition++
            }
            mediaPlayer?.setDataSource(mList[mCurrentPlayPosition])
            mediaPlayer?.prepareAsync()
            mediaPlayer?.setOnPreparedListener {
                mediaPlayer?.start()
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}

框架ExoPlayer

ExoPlayer是运行在YouTube app Android版本上的视频播放器而并非系统内置的API,所以相对于MediaPlayer来说,其功能更加强大,除了MediaPlayer本身所支持的格式外,ExoPlayer还可以支持DASH和SmoothStreaming格式。接下来我们就一起来看看吧。

ExoPlayer库的核心是ExoPlayer接口。ExoPlayer公开了传统的高水平媒体播放器的功能,例如媒体缓冲,播放,暂停和快进功能。ExoPlayer实现旨在对正在播放的媒体类型,存储方式和位置以及渲染方式做出一些假设(因此几乎没有限制)。ExoPlayer没有直接实现媒体文件的加载和渲染,而是把这些工作委托给了在创建播放器或者播放器准备好播放的时候注入的组件。所有ExoPlayer实现的通用组件有:

(1) ExoPlayer:创建ExoPlayer的实现实例

 if (mExoPlayer == null) {
     // 创建ExoPlayer的实例
     mExoPlayer = ExoPlayerFactory.newSimpleInstance(this, DefaultRenderersFactory(this), DefaultTrackSelector(), DefaultLoadControl())
     // 添加ExoPlayer的监听
     mExoPlayer!!.addListener(this)
}

对于ExoPlayer来说,其创建实例的时候后面的几个参数是比较重要的,我们点击进去看下源码里面

  public static SimpleExoPlayer newSimpleInstance(
      Context context,
      RenderersFactory renderersFactory,
      TrackSelector trackSelector,
      LoadControl loadControl) {
    return newSimpleInstance(
        context,
        renderersFactory,
        trackSelector,
        loadControl,
        /* drmSessionManager= */ null,
        Util.getLooper());
  }

参数1:Context;对应的是上下文实例,这个没什么好说的。
参数2:RenderersFactory;渲染器,用于渲染媒体文件。当创建播放器的时候,Renderers被注入。
参数3:TrackSelector ;轨道选择器,用于选择MediaSource提供的轨道(tracks),供每个可用的渲染器使用。
参数4:LoadControl ;用于控制MediaSource何时缓冲更多的媒体资源以及缓冲多少媒体资源。

ExoPlayer库提供了在普通使用场景下上述组件的默认实现。ExoPlayer可以使用这些默认的组件,也可以使用自定义组件。例如可以注入一个自定义的LoadControl用来改变播放器的缓存策略,或者可以注入一个自定义渲染器以使用Android本身不支持的视频解码器。

(2) MediaSource:媒体资源
用于定义要播放的媒体,加载媒体,以及加载媒体的路径,具体的写法为:

  val uri = Uri.parse(mList!![mPlayBackPosition])
  val mediaSource = ProgressiveMediaSource.Factory(DefaultHttpDataSourceFactory("exoplayer-codelab")).createMediaSource(uri)
  // 设置好MediaSource之后通过prepare进行注入
  mExoPlayer!!.prepare(mediaSource)

注意Uri.parse()里面设置的值可以是本地音频的路径也可以是网络音频的路径。

了解了ExoPlayer的基本信息后,接下来我们看看到底应该怎么去使用。

ExoPlayer的使用
第一步:添加依赖
 implementation 'com.google.android.exoplayer:exoplayer:2.10.5'

为了省事,我们依赖了整个ExoPlayer库。你也可以只依赖你真正需要的库。例如果你要播放DASH类型的媒体资源,你可以只依赖Core,DASH,UI这三个库。即

implementation 'com.google.android.exoplayer:exoplayer-core:2.10.5'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.10.5'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.10.5'

整个ExoPlayer库包括5个子库,依赖了整个ExoPlayer库和依赖5个子库是等效的。

exoplayer-core:核心功能 (必要)
exoplayer-dash:支持DASH内容
exoplayer-hls:支持HLS内容
exoplayer-smoothstreaming:支持SmoothStreaming内容
exoplayer-ui:用于ExoPlayer的UI组件和相关的资源。
第二步:创建ExoPlayer的实例
    private var mExoPlayer: ExoPlayer? = null
    if (mExoPlayer == null) {
            mExoPlayer = ExoPlayerFactory.newSimpleInstance(this, DefaultRenderersFactory(this), DefaultTrackSelector(), DefaultLoadControl())
            mExoPlayer!!.addListener(this)
    }
第三步:创建媒体资源类MediaSource并装载资源文件
   val uri = Uri.parse(mList!![mPlayBackPosition])
   val mediaSource = ProgressiveMediaSource.Factory(DefaultHttpDataSourceFactory("exoplayer-codelab")).createMediaSource(uri)
   mExoPlayer!!.prepare(mediaSource)
第四步:开始播放
mExoPlayer!!.playWhenReady = true

需要强调的是ExoPlayer的播放并没有像MediaPlayer一样通过很明显的start()、pause()、stop()方法去实现,而是统一使用void setPlayWhenReady(boolean playWhenReady)方法来实现,true代表的就是播放/继续播放,false代表的就是暂停/停止

至此,一个完整的播放流程就结束了,当然除此之外还有一些比较重要的知识点,如下所示。

知识点一:ExoPlayer的API

prepare(MediaSource mediaSource):装载资源文件
getDuration():获取流媒体的总播放时长,单位是毫秒。
setPlayWhenReady(boolean playWhenReady):播放/暂停/继续播放
seekTo(long positionMs):设置当前ExoPlayer的播放位置,单位是毫秒。
isPlaying():当前是否在播放
stop():停止播放
getCurrentPosition():获取当前ExoPlayer的播放位置,单位是毫秒。
getRepeatMode():获取当前是否循环播放
release():释放播放资源

setRepeatMode(int value):设置是否循环播放,可供取值为(1)Player.REPEAT_MODE_ONE(不允许循环播放,只会播放一次)
(2)Player.REPEAT_MODE_ALL(循环播放)

setPlaybackParameters:设置播放倍速
PlaybackParameters playbackParameters = new PlaybackParameters(speed, 1.0F);
mExoPlayer.setPlaybackParameters(playbackParameters);

当然,除了列举的这些API,还有其他的很多的API,这就需要我们平时在使用的过程中去把握了。

知识点二:ExoPlayer的回调

interface EventListener {

    //播放总时间线改变,这里可用于设置播放总时长
    default void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) { 
    }

    //播放资源有改变
    default void onStaticMetadataChanged(List<Metadata> metadataList) {}

    
    //是否在加载
    default void onIsLoadingChanged(boolean isLoading) {
      onLoadingChanged(isLoading);
    }
 
    //播放器播放状态改变,查看 State 有IDLE,BUFFERING加载中, READY 资源准备好, ENDED 已结束 
    default void onPlaybackStateChanged(@State int state) {}

   //视频资源准备好就播放的设置改变    
    default void onPlayWhenReadyChanged(
        boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {}
 
    //播放状态改变,开始播放或暂停
    default void onIsPlayingChanged(boolean isPlaying) {}

    //重复播放的模式改变 
    default void onRepeatModeChanged(@RepeatMode int repeatMode) {}
 
    default void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {}

    //播放器报错
    default void onPlayerError(ExoPlaybackException error) {}
 

    //参数改变    
    default void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {}
 
     
    default void onEvents(Player player, Events events) {}
  }

知识点三:ExoPlayer的回收
使用完ExoPlayer需要回收资源。ExoPlayer是很消耗系统资源的,所以在使用完ExoPlayer,不要等待系统自动回收,最好是主动回收资源。

if (mExoPlayer!= null) {
           mExoPlayer.stop();
           mExoPlayer.release();
           mExoPlayer= null;
}

知识点四:ExoPlayer的小例子

mediaPlayer.jpg

其实在实际开发中,我们应该将播放的逻辑与Service相结合,这里为了测试效果就没有写的那么完整

/**
 * 框架ExoPlayer制作简单的媒体播放器
 */
class MainActivity : AppCompatActivity(), Player.EventListener {

    private var mList: MutableList<String>? = null
    private var mExoPlayer: ExoPlayer? = null
    private var mPlayWhenReady: Boolean = false
    private var mPlayBackPosition = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initMusic()
        initializePlayer()
    }

    /**
     * 加载音频文件
     */
    private fun initMusic() {
        mList = ArrayList()
        mList!!.add("https://xysx-voice.oss-cn-shanghai.aliyuncs.com/audio/2e76a133b898a2f6c8fb62e963b87ce4_tongkuercanlandeyisheng.mp3")
        mList!!.add("https://xysx-voice.oss-cn-shanghai.aliyuncs.com/audio/e845a564e795c628c6bdd652ddbc24e3_fulidadeqingshaonianshidai.mp3")
        mList!!.add("https://xysx-voice.oss-cn-shanghai.aliyuncs.com/audio/e9b9abdcc3855586ec2c4651293c11e9_abuyuwulei.mp3")
        mList!!.add("https://xysx-voice.oss-cn-shanghai.aliyuncs.com/audio/ba2eaf007614fdf81c8d9e895ba88ff2_abuchuangzuodekaishi.mp3")
    }

    /**
     * 初始化媒体播放器
     */
    private fun initializePlayer() {
        if (mExoPlayer == null) {
            mExoPlayer = ExoPlayerFactory.newSimpleInstance(this, DefaultRenderersFactory(this), DefaultTrackSelector(), DefaultLoadControl())
            mExoPlayer!!.addListener(this)
        }
        // 创建一个音频文件
        val uri = Uri.parse(mList!![mPlayBackPosition])
        val mediaSource = ProgressiveMediaSource.Factory(DefaultHttpDataSourceFactory("exoplayer-codelab")).createMediaSource(uri)
        mExoPlayer!!.prepare(mediaSource)
        mExoPlayer!!.playWhenReady = mPlayWhenReady
    }

    /**
     * 释放媒体资源
     */
    private fun releasePlayer() {
        if (mExoPlayer != null) {
            mExoPlayer!!.stop()
            mExoPlayer!!.release()
            mExoPlayer = null
        }
    }

    /**
     * 上一首
     */
    fun lastMusic(view: View) {
        releasePlayer()
        mPlayWhenReady = true
        if (mPlayBackPosition == 0) {
            mPlayBackPosition = mList!!.size - 1
        } else {
            mPlayBackPosition--
        }
        initializePlayer()
    }


    /**
     * 播放
     */
    fun startMusic(view: View) {
        mPlayWhenReady = true
        mExoPlayer!!.playWhenReady = mPlayWhenReady
    }

    /**
     * 暂停
     */
    fun pauseMusic(view: View) {
        mPlayWhenReady = false
        mExoPlayer!!.playWhenReady = mPlayWhenReady
    }

    /**
     * 结束
     */
    fun stopMusic(view: View) {
        mPlayWhenReady = false
        mExoPlayer!!.stop()
        mExoPlayer!!.release()
    }


    /**
     * 下一首
     */
    fun nextMusic(view: View) {
        releasePlayer()
        mPlayWhenReady = true
        if (mPlayBackPosition == mList!!.size - 1) {
            mPlayBackPosition = 0
        } else {
            mPlayBackPosition++
        }
        initializePlayer()
    }

     /**
     * 播放状态监听
     */
    override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
        var stateString: String? = null
        if (playWhenReady && playbackState === Player.STATE_READY) {
            Log.d("zhoufan", "onPlayerStateChanged: actually playing media")
        }
        stateString = when (playbackState) {
            Player.STATE_IDLE -> "ExoPlayer.STATE_IDLE      -"
            Player.STATE_BUFFERING -> "ExoPlayer.STATE_BUFFERING -"
            Player.STATE_READY -> {
                "ExoPlayer.STATE_READY     -"
            }
            Player.STATE_ENDED -> "ExoPlayer.STATE_ENDED     -"
            else -> "UNKNOWN_STATE             -"
        }
        Log.d("zhoufan", "changed state to $stateString playWhenReady: $playWhenReady")
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容

  • 一、流媒体 什么是流媒体技术?简单的说,就是边下载,边播放。也就是说,客户端在播放前,无需下载整个媒体文件,而是在...
    szy啊阅读 16,031评论 2 23
  • Android的多媒体框架支持各种常见的多媒体类型,这样在程序中可以很容易地集成音频、视频或者图片。Android...
    _Ryan阅读 61,259评论 13 54
  • Android三种播放视频的方式(以下内容大多使用真机测试,所以没有运行图片,大家可以自己实战看看) 1、使用其自...
    酷酷的Demo阅读 291评论 0 1
  • MediaPlayer是Android中播放视频最简单的控件,这篇文章将介绍MediaPlaer播放视频的基本方法...
    iso8859_1阅读 2,243评论 0 0
  • 浅谈Android MediaPlayer 前言 MediaPlayer是Android中多媒体框架中一个重要的组...
    sunnyslxie阅读 213评论 0 1