视频播放的⼯作终于告⼀段落了,由于之前对ffmpeg, vlc,流媒体这些东西完全没有接触过,所以很⾟苦的查资料,艰难的理解⼀点点的概念,还好,总算有了⼀个可⾏的解决⽅法,算是最简单的,不⽤对解码库有深⼊的了解,算是⼊门的⽅法。
⼀、应⽤场景:
EPUB书中嵌⼊视频,要求点击图⽚时能够打开相应的视频进⾏播放,视频的格式为mp4
⼆、解决⽅法:
1.选⽤播放器,最开始采⽤github上的开源⼯程 havlenapetr/ffmpeg 使⽤cygwin + NDK r8编译通过,在真机和模拟器上播放都正常,但是这个库有⼏个缺陷,最致命的是它运⾏需要依赖android的libjniaudio.so和libjnivideo.so两个基础库,⽽这两个库是与系统版本有关的,这就意味着要针对不同的版本编译不同的基础库,这给将来的应⽤带来了很⼤⿇烦,另⼀个缺陷是⼤屏幕播放图像倾斜的问题,但是这个问题在⽹上有⼈已经解决了,修改onVideoSizeChangedListener的实现即可解决,具体参照这篇博⽂。
最后到了vlc这个解码库,它⽐ffmpeg还要庞⼤,ffmpeg只是它的⼀部分⽽已,但是它的好处是各种an
droid版本都⽀持的⽐较好,所以就采⽤它了。在ubuntu 10 + ruby 1.9.2 + NDK r5c下编译通过,播放本地视频⼀切正常。代码下载地址
完成了这⼀步,整个⼯作就完成了⼀半,另外⼀半是如何播放EPUB书中的mp4视频? 这⾥边有两个问题:
1. 我们知道,EPUB其实就是⼀个zip⽂件,那么为了减少缓冲时间,最好的⽅式是把mp4不压缩存储在zip⽂件中,播放时直接把⽂件指针seek到mp4开始的位置开始加载,⽤播放本地⽂件的⽅式来解码,但是看了faplayer提供的接⼝后我放弃了,faplayer提供了类似android⾃带的MediaPlayer的接⼝,数据加载只有⼀个setDataSource(String path)的接⼝。完全没有办法加载⽂件指针,所以我们退⼀步,把mp4缓存到本地来播放,这样的话为了减少播放前的等待时间,我们不能等到Mp4加载全部完成后再来播放,只能边缓冲边播放,这就带来了第⼆个问题:
2.我们知道,mp4的结构信息存储在⼀个叫moov的box中,我们⾄少要等到moov这个box被加载后(当然还得有⼀部分真实的⾳视频数据也得被加载)后,才能正常进⾏播放,⽽真实的mp4中,有很多是吧moov放置在⽂件的尾部的,我猜测是便于⽣成数据,避免产⽣moov box 对后⾯数据的偏移产⽣影响。这就需要预先对mp4进⾏调整(预先我指的是在制作EPUB书的时候),在参考上的⼀篇博⽂后,我到了qt-faststart 这个⼯具,它可以把moov box调整到⽂件的前部,后来我到了⼀个更好的⼯具,我们在后⾯再说。
调整完moov后还有⼀个问题:moov中记录了mp4有多少个track,track中记录的是真实的⾳视频数据存储的位置,⽽真实数据存储在mdat 中,⽽且经常是⾳频,视频分开放置的,⽽且有可能相隔的⽐较远,这样的话就会出现⼀个问题:在播放的时候⾳频或是视频数据被丢掉了,要避免这个问题,我们需要对mp4做⼀个处理,这个处理就叫做"流化",经过搜索,我到了⼀个很好的⼯具,那就是VLC Media Player 下载地址它提供了很好的流化视频的功能,在流化的时候会把moov提到⽂件前⾯去,所以它⽐qt-faststart这个⼯具要强很多,完成这个后我们的播放就基本上没有技术难题了
好了,说了这么多,放出⽰例代码,代码参照faplayer:
1public class VideoPlayerActivity extends Activity implements
2 AbsMediaPlayer.OnBufferingUpdateListener,
3 AbsMediaPlayer.OnCompletionListener, AbsMediaPlayer.OnErrorListener,
4 AbsMediaPlayer.OnInfoListener, AbsMediaPlayer.OnPreparedListener,
5 AbsMediaPlayer.OnProgressUpdateListener,
6 AbsMediaPlayer.OnVideoSizeChangedListener, OnTouchListener,
7 OnClickListener, OnSeekBarChangeListener {
8
9static final String LOGTAG = "DANMAKU-PlayerActivity";
10
11private static final int SURFACE_NONE = 0;
12private static final int SURFACE_FILL = 1;
13private static final int SURFACE_ORIG = 2;
14private static final int SURFACE_4_3 = 3;
15private static final int SURFACE_16_9 = 4;
16private static final int SURFACE_16_10 = 5;
17private static final int SURFACE_MAX = 6;
18
19private static final int MEDIA_PLAYER_BUFFERING_UPDATE = 0x4001;
20private static final int MEDIA_PLAYER_COMPLETION = 0x4002;
21private static final int MEDIA_PLAYER_ERROR = 0x4003;
22private static final int MEDIA_PLAYER_INFO = 0x4004;
23private static final int MEDIA_PLAYER_PREPARED = 0x4005;
24private static final int MEDIA_PLAYER_PROGRESS_UPDATE = 0x4006;
25private static final int MEDIA_PLAYER_VIDEO_SIZE_CHANGED = 0x4007;
26
27private static final int VIDEO_STATE_UPDATE = 0x4008;
28private static final int VIDEO_CACHE_READY = 0x4009;
29private static final int VIDEO_CACHE_UPDATE = 0x400A;
30private static final int VIDEO_CACHE_FINISH = 0x400B;
31
32public static final String REMOTE_URL = "url";
33
34private static final int READY_BUFFER_LENGTH = 0;
35
36private static final int CACHE_BUFFER_LENGTH = 0;
37
38/* the media player */
39private AbsMediaPlayer mMediaPlayer = null;
40
雪 王艺翔41/* GUI evnet handler */
42private Handler mEventHandler;
43
44/* player misc */
45private ProgressBar mProgressBarPreparing;
46
47/* player controls */
48private TextView mTextViewTime;
49private SeekBar mSeekBarProgress;
田馥甄绯闻男友50private TextView mTextViewLength;
51private ImageButton mImageButtonToggleMessage;
52private ImageButton mImageButtonSwitchAudio;
53private ImageButton mImageButtonSwitchSubtitle;
54private ImageButton mImageButtonPrevious;
55private ImageButton mImageButtonTogglePlay;
56private ImageButton mImageButtonNext;
57private ImageButton mImageButtonSwitchAspectRatio;
58
59private LinearLayout mLinearLayoutControlBar;
60
61/* player video */
62private SurfaceView mSurfaceViewVlc;
63private SurfaceHolder mSurfaceHolderVlc;
64
65/* misc */
66private boolean mMediaPlayerLoaded = false;
67private boolean mMediaPlayerStarted = false;
68
69/* misc */
70private int mTime = -1;
71private int mLength = -1;
72private boolean mCanSeek = true;
73private int mAspectRatio = 0;
74
75private int mAudioTrackIndex = 0;
76private int mAudioTrackCount = 0;
77private int mSubtitleTrackIndex = 0;
78private int mSubtitleTrackCount = 0;
79
80private String localUri = null;
81private String remoteUrl = null;
82private long mediaLength = 0;
83
84private int readSize;
85private boolean isReady;
86private int errorCount;
87
88protected boolean isError;
89
90protected int curPosition;
91
92private String cacheFilePath;
93
94protected void initializeEvents() {
95 mEventHandler = new Handler() {
96public void handleMessage(Message msg) {
97switch (msg.what) {
98case MEDIA_PLAYER_BUFFERING_UPDATE: {
99if (mMediaPlayerLoaded) {
100 mProgressBarPreparing
101 .setVisibility(msg.arg1 < 100 ? View.VISIBLE
102 : View.GONE);
103 }
104 }
105break;
106case MEDIA_PLAYER_COMPLETION: {
107 curPosition = 0;
108 mMediaPlayer.pause();
109 }
110break;
111case MEDIA_PLAYER_ERROR: {
112 mMediaPlayerLoaded = true;
113 isError = true;
114 errorCount++;
115 mMediaPlayer.pause();
116 mProgressBarPreparing.setVisibility(View.VISIBLE);
117 }
118break;
119case MEDIA_PLAYER_INFO: {
120if (msg.arg1 == MediaPlayer.MEDIA_INFO_NOT_SEEKABLE) {
121 mCanSeek = false;
122 }
123 }
124break;
125case MEDIA_PLAYER_PREPARED: {
126 mProgressBarPreparing.setVisibility(View.GONE);林心如男友
127 mMediaPlayerLoaded = true;
128 mMediaPlayer.seekTo(curPosition);
129 startMediaPlayer();
130 }
131break;
132case MEDIA_PLAYER_PROGRESS_UPDATE: {
133if (mMediaPlayer != null) {
134int length = msg.arg2;
135if (length >= 0) {
136 mLength = length;
137 mTextViewLength.setText(SystemUtility
138 .getTimeString(mLength));
139 mSeekBarProgress.setMax(mLength);
140 }
141int time = msg.arg1;
142if (time >= 0) {
143 mTime = time;
144 mTextViewTime.setText(SystemUtility
145 .getTimeString(mTime));
146 mSeekBarProgress.setProgress(mTime);
147 }
148 }
149 }
150break;
151case MEDIA_PLAYER_VIDEO_SIZE_CHANGED: {
152 AbsMediaPlayer player = (AbsMediaPlayer) msg.obj;
153 SurfaceView surface = mSurfaceViewVlc;
154int ar = mAspectRatio;
155 changeSurfaceSize(player, surface, ar);
156 }
157break;
158
夏目友人帐第二季ed159case VIDEO_STATE_UPDATE: // 缓冲启动
160break;
161case VIDEO_CACHE_READY: { // 缓冲完成最低限度
162 isReady = true;
163 mMediaPlayer.setDataSource(localUri);
164 mMediaPlayer.prepareAsync();
165 }
166break;
167case VIDEO_CACHE_UPDATE: { // 缓冲加载
168if (isError) {
169 mMediaPlayer.setDataSource(localUri);
170 mMediaPlayer.prepareAsync();
171 isError = false;
172 }
173 }
174break;
175case VIDEO_CACHE_FINISH: { // 缓冲结束
176if (isError) {
177 mMediaPlayer.setDataSource(localUri);
178 mMediaPlayer.prepareAsync();
179 isError = false;
180 }
181 }
182break;
183default:
184break;
185 }
186
187super.handleMessage(msg);
188 }
189 };
190 }
191
192protected void initializeControls() {
193/* SufaceView used by VLC is a normal surface */
194 mSurfaceViewVlc = (SurfaceView) findViewById(R.id.player_surface_vlc); 195 mSurfaceHolderVlc = Holder();
196 mSurfaceHolderVlc.setType(SurfaceHolder.SURFACE_TYPE_NORMAL); 197 mSurfaceHolderVlc.addCallback(new SurfaceHolder.Callback() {
穿越时空的少女主题曲198 @Override
199public void surfaceCreated(SurfaceHolder holder) {
200 createMediaPlayer(false, localUri, mSurfaceHolderVlc);
201 }
202
203 @Override
204public void surfaceChanged(SurfaceHolder holder, int format,
205int width, int height) {
206 mMediaPlayer.setDisplay(holder);
207 }
208
209 @Override
210public void surfaceDestroyed(SurfaceHolder holder) {
211 destroyMediaPlayer(false);
212 }
213
214 });
215 mSurfaceViewVlc.setOnTouchListener(this);
216
217 mTextViewTime = (TextView) findViewById(R.id.player_text_position);
218 mSeekBarProgress = (SeekBar) findViewById(R.id.player_seekbar_progress);
219 mSeekBarProgress.setOnSeekBarChangeListener(this);
220 mTextViewLength = (TextView) findViewById(R.id.player_text_length);
221 mImageButtonToggleMessage = (ImageButton) findViewById(R.id.player_button_toggle_message);
222 mImageButtonToggleMessage.setOnClickListener(this);
223 mImageButtonSwitchAudio = (ImageButton) findViewById(R.id.player_button_switch_audio);
224 mImageButtonSwitchAudio.setOnClickListener(this);
225 mImageButtonSwitchSubtitle = (ImageButton) findViewById(R.id.player_button_switch_subtitle);
226 mImageButtonSwitchSubtitle.setOnClickListener(this);
227 mImageButtonPrevious = (ImageButton) findViewById(R.id.player_button_previous);
228 mImageButtonPrevious.setOnClickListener(this);
229 mImageButtonTogglePlay = (ImageButton) findViewById(R.id.player_button_toggle_play);
230 mImageButtonTogglePlay.setOnClickListener(this);
231 mImageButtonNext = (ImageButton) findViewById(R.id.player_button_next);
232 mImageButtonNext.setOnClickListener(this);
233 mImageButtonSwitchAspectRatio = (ImageButton) findViewById(R.id.player_button_switch_aspect_ratio); 234 mImageButtonSwitchAspectRatio.setOnClickListener(this);
235
236 mLinearLayoutControlBar = (LinearLayout) findViewById(R.id.player_control_bar);
237
238 mProgressBarPreparing = (ProgressBar) findViewById(R.id.player_prepairing);
239 }
240
241protected void initializeData() throws IOException {
242 Intent intent = getIntent();
243 remoteUrl = StringExtra(REMOTE_URL);
244if (remoteUrl == null) {
245 finish();
246 }
247
248if (remoteUrl.startsWith("file:")) {
249 localUri = remoteUrl;
super girl super junior250 } else {
251 cacheFilePath = Instance().getMediaCachePath() + "cache.mp4";
252 File cacheFile = new File(cacheFilePath);
253if (ists()) {
254 cacheFile.delete();
255 }
256
257 ParentFile().mkdirs();
258 ateNewFile();
259 localUri = Uri.fromFile(cacheFile).toString();
260
261new Thread(new Runnable() {
262
263 @Override
264public void run() {
265 startBufferData();
266 }
267 }).start();
268 }
269 }
270
271private void startBufferData() {
272 InputStream is = null;
273 FileOutputStream out = null;
274try {
275 out = new FileOutputStream(cacheFilePath);
276if (URLUtil.isNetworkUrl(remoteUrl)) {
277 URL url = new URL(remoteUrl);
278 HttpURLConnection httpConnection = (HttpURLConnection) url
279 .openConnection();
280 is = InputStream();
281 mediaLength = ContentLength();
282 } else {
283// TODO fix this, very very bad idea
284 Book book = Instance().getBook();
285 is = EntryInputStream(remoteUrl);
286 mediaLength = EntrySize(remoteUrl);
287 }
288
289if (is == null || mediaLength == -1) {
290return;
291 }
292
293byte[] buf = new byte[4 * 1024];
294int size = 0;
295int lastReadSize = 0;
296
297 mEventHandler.sendEmptyMessage(VIDEO_STATE_UPDATE);
298
299while ((size = is.read(buf)) != -1) {
300try {
301 out.write(buf, 0, size);
302 readSize += size;
303 } catch (IOException e) {
304 e.printStackTrace();
305 }
306
307if (!isReady) {
308if (readSize - lastReadSize > READY_BUFFER_LENGTH
309 || readSize - lastReadSize == mediaLength) {
310 lastReadSize = readSize;
311int timeCount = 0;
312while (timeCount < 10) {
313if (mMediaPlayer != null) {
314 mEventHandler
315 .sendEmptyMessage(VIDEO_CACHE_READY);
316break;
317 } else {
318try {
319 Thread.sleep(1000);
320 } catch (InterruptedException e) {
321// TODO Auto-generated catch block
322 e.printStackTrace();
323 }
324 timeCount++;
325 }
326 }
327 }
328 } else {
329if (readSize - lastReadSize > CACHE_BUFFER_LENGTH
330 * (errorCount + 1)) {
331 lastReadSize = readSize;
332 mEventHandler.sendEmptyMessage(VIDEO_CACHE_UPDATE); 333 }
334 }
335 }
336
337 mEventHandler.sendEmptyMessage(VIDEO_CACHE_FINISH);
338 } catch (IOException e) {
339 e.printStackTrace();
340 } finally {
341if (out != null) {
342try {
343 out.close();
344 } catch (IOException e) {
345// TODO Auto-generated catch block
346 e.printStackTrace();
347 }
348 }
349
350if (is != null) {
351try {
352 is.close();
353 } catch (IOException e) {
354// TODO Auto-generated catch block
355 e.printStackTrace();
356 }
357 }
358 }
359 }
360
361protected void resetMediaPlayer() {
362int resource = -1;
363/* initial status */
364 mMediaPlayerLoaded = false;
365 mTime = -1;
366 mLength = -1;
367 mCanSeek = true;
368 mAspectRatio = 0;
369/**/
370 mImageButtonToggleMessage.setVisibility(View.GONE);
371 mImageButtonSwitchAudio.setVisibility(View.GONE);
372 mImageButtonSwitchSubtitle.setVisibility(View.GONE);
发布评论