Tìm hiểu về movie Maker trên android - Phần 1
Giới thiệu chung Việc tạo video trên danh sách ảnh như các ứng dụng trên PC đã không còn xa lạ và vô cùng độc đáo với bất kể một ai mong muốn hướng tới 1 cái đẹp Những hiệu ứng sắc nét như Proshowgold hay đơn giản như MovieMaker - Ứng dụng default trên Window XP 2003 Để tạo ra 1 video PC đã quá ...
Giới thiệu chung
Việc tạo video trên danh sách ảnh như các ứng dụng trên PC đã không còn xa lạ và vô cùng độc đáo với bất kể một ai mong muốn hướng tới 1 cái đẹp Những hiệu ứng sắc nét như Proshowgold hay đơn giản như MovieMaker - Ứng dụng default trên Window XP 2003 Để tạo ra 1 video PC đã quá dễ dàng dường như chỉ cần 1 cú click chuột là chúng ta đã có thể thưởng thức được thành quả. Tuy vậy nhưng trên android cũng không quá khó khăn để tạo ra 1 ứng dụng như vậy
"MovieMaker" sẽ chia thành 6 phần hướng dẫn đầy đủ và chi tiết (kèm code demo)
- Mô tả chức năng của ứng dụng
- Tạo nền draw của ứng dụng để preview và xuất ra video
- Vẽ ảnh, text và tạo hiệu ứng của chúng
- Thêm music bên ngoài vào video
- Effect cho video
- Export video
Ở phần 1 mình sẽ diễn giải nội dung 1, 2 các phần còn lại sẽ được tiếp tục viết chi tiết ở phần sau
**1. Mô tả chức năng của ứng dụng **
- Ứng dụng cho phép tạo ra video từ danh sách ảnh chọn từ thư viện của máy
- Thêm text cho từng ảnh
- Tạo hiệu ứng cơ bản cho ảnh và text: zoom, translate, alpha, rotate 3D
- Thêm music vào video
- Xuất video cho phép chọn chất lượng tương ứng: 1080 Full HD, 720, 480,...
- Cho phép share video khi xuất
- Và nhiều tiện ích khác đi kèm
**2. Tạo nền draw để preview và xuất ra video **
Ở bài trước mình có viết demo hướng dẫn xuất video sang các chất lượng khác nhau sử dụng MediaFormat để decode và encode video, sử dụng Muxer để tạo video
Và ở đây cũng vậy, chúng ta sử dụng Opengl ES 20 bộ thư viện trên android 4.3 để vẽ hình ảnh + text
Trước hết ta sẽ sử dụng MediaCodec để encode video và frame nền cho ứng dụng trong khi vẽ, người dùng có thể preview hoặc export video một cách dễ dạng
Kiểm tra device MediaCodec có được hỗ trợ trong việc encode H264 (video mp4) trước khi prepareEncode
private boolean isSoftwareCodec(MediaCodec codec) { return codec.getCodecInfo().getName().equals("OMX.google.h264.encoder"); }
Xác định chất lượng và thuộc tính của video khi export
protected void prepareEncoder(String mimeType, int awidth, int height, int bitRate, int framesPerSecond, File outputFile) throws IOException { /** Tạo MediaFormat với các thuộc tính encoder -> Video MP4 */ MediaFormat format = MediaFormat.createVideoFormat(mimeType, awidth, height); // Add màu nền cho frame format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); // Add bitrate cho video format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); // Số lượng frame trên giây trung bình với video cơ bản là 28 - càng lớn video sẽ càng nét nhưng sẽ rất tốn tài nguyên và dung lượng lớn format.setInteger(MediaFormat.KEY_FRAME_RATE, framesPerSecond); // Chỉnh iFrame cho video format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL); /** Khởi tạo MediaCodec cho việc encoder */ mEncoder = MediaCodec.createEncoderByType(mimeType); mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); /** Tạo surface để vẽ các object lên */ surface = mEncoder.createInputSurface(); /** Prepare cho vẽ object và starting encode */ mEglCore = new EglCore(null, EglCore.FLAG_RECORDABLE); mInputSurface = new WindowSurface(mEglCore, surface, true); // Khởi động Surface và EGL mInputSurface.makeCurrent(); // Lắng nghe encoder mEncoder.start(); // Khởi tạo video đầu ra với các thuộc tính của MediaFormat bên trên mMuxer = new MediaMuxer(outputFile.toString(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); // Chuẩn bị cho việc xuất video mTrackIndex = -1; mMuxerStarted = false; }
Như vậy ta đã tạo được video đầu ra với outputFile. Nếu không muốn export ta sẽ không cần sử dụng mMuxer.start(), nếu chỉ cần preview ta sẽ add surface vào 1 view nào đó SurfaceView hoặc FrameLayout
Update video khi draw từng frame trong nano second
protected void submitFrame(long presentationTimeNsec) { mInputSurface.setPresentationTime(presentationTimeNsec); mInputSurface.swapBuffers(); }
Bước khá quan trọng đó là drainEncoder, get ByteBuffer từ VideoEncoder (mEncoder)
protected void drainEncoder(boolean endOfStream) { final int TIMEOUT_USEC = 10000; // Thời gian refresh từng object lên tới nanosecond // export to end of stream if (endOfStream) { mEncoder.signalEndOfInputStream(); } /** Get ByteBuffer từ VideoEncoder và ghi nó ra MediaMuxer*/ ByteBuffer[] encoderOutputBuffers = mEncoder.getOutputBuffers(); while (true) { int encoderStatus = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC); // Không có dữ liệu đầu vào if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { if (!endOfStream) { break; // out of while } } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { // Dữ liệu đang được xuất ra ByteBuffer encoderOutputBuffers = mEncoder.getOutputBuffers(); } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { MediaFormat newFormat = mEncoder.getOutputFormat(); // Ghi video đầu ra (outputFile) mTrackIndex = mMuxer.addTrack(newFormat); mMuxer.start(); mMuxerStarted = true; } else if (encoderStatus < 0) { // data ignore } else { ByteBuffer encodedData = encoderOutputBuffers[encoderStatus]; if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { mBufferInfo.size = 0; } if (mBufferInfo.size != 0) { // Encode to position encodedData.position(mBufferInfo.offset); encodedData.limit(mBufferInfo.offset + mBufferInfo.size); // Ghi dữ liệu mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo); } mEncoder.releaseOutputBuffer(encoderStatus, false); if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { // Out of file } } } }
Như vậy ta đã tạo xong GeneratedMovie có thể export video hoặc get Surface cho preview tuỳ thích
Sau khi có frame để draw việc bây giờ cần tạo khởi tạo OpenGL ES cho việc draw object
- prepareFrame
- updateFrame
- draw
- doFrame
prepareFrame
private void prepareFramebuffer(int awidth, int height) { // Create texture object and textureID GLES20.glGenTextures(1, values, 0); GlUtil.checkGlError("glGenTextures"); mOffscreenTexture = values[0]; // expected > 0 GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mOffscreenTexture); // Create texture storage /** * GLES20.GL_TEXTURE_2D thuộc tính cho việc vẽ đối tượng: shape, text, bitmap, ... * Trong buổi trước mình sử dụng GLES11Ext.GL_TEXTURE_EXTERNAL_OES để gender bytebuffer */ GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, awidth, height, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null); // Tạo frame object và lắng nghe dữ liệu của chúng GLES20.glGenFramebuffers(1, values, 0); mFramebuffer = values[0]; // expected > 0 GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFramebuffer); // Tạo depth buffer và lắng nghe data GLES20.glGenRenderbuffers(1, values, 0); mDepthBuffer = values[0]; // expected > 0 GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, mDepthBuffer); // allowcate storage GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER, GLES20.GL_DEPTH_COMPONENT16, awidth, height); // Gắn buffer và màu sắc cho frame GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT, GLES20.GL_RENDERBUFFER, mDepthBuffer); GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, mOffscreenTexture, 0); // Check status int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER); // Quay lai buffer default GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); }
update Với việc vẽ ảnh, ví dụ như trong 3 giây đầu vẽ ảnh đầu tiên với các hiệu ứng như ảnh chạy từ trái sang phải
Như vậy tại mỗi frame ta cần cập nhật lại vị trí của ảnh, kích cỡ, alpha, ...
Mỗi giây hoặc frame ta cần xác định cần vẽ ảnh nào ở vị trí nào, function update sẽ thực hiện các thao tác đó và cập nhật chính xác vị trí cho từng ảnh để việc animation không bị lag giật ngoài mong muốn
private void update(long timeStampNanos) { /** * Update position * Update image * And animation object */ }
draw Vẽ ảnh, object lên surface thông qua OpenGL ES 20
private void draw() { // Tạo màu nền, xoá các dữ liệu buffer GLES20.glClearColor(0.2f, 0.2f, 0.2f, 1.0f); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); // Create filter texture GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST); GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT); GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT); // Draw bitmap in position // Ngoài ra có thể sử dụng matrix cho animation của object GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, position); }
doFrame
private void doFrame(long timeStampNanos) { // Controller cho việc vẽ ảnh, cập nhật vị trí của ảnh, animation tương ứng }
Bài viết mới dừng lại ở 2 phần
- Tạo frame recorder cho việc export hoặc preview cho video
- Tạo Controller, setup OpenGL ES để draw object với từng vị trí, animation
Như vậy nếu muốn tạo ra video với n ảnh được chọn Sau khi khởi tạo và constuctor 1 số thuộc tính của video, ta sẽ có được số frame và thời gian chính xác đến nanosecond của video đầu ra
Chỉ cần hàm while(frame to end) ta vẽ và thay đổi ảnh ở từng vị trí tương ứng
Với preview sau khi vẽ chỉ cần get surface trong GeneratedMovie và add chúng vào SurfaceView ta sẽ có được preview tương tự như đang open video
Còn với export video thì cần goi đến drainEncoder trong GeneratedMovie từ Controller
Ở bài sau sẽ có chi tiết hơn về animation và code demo ban đầu