采⽤faplayer播放EPUB书中的mp4视频
视频播放的⼯作终于告⼀段落了,由于之前对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
夏目友人帐第二季ed
159case 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 junior
250        } 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);