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á 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)

  1. Mô tả chức năng của ứng dụng
  2. Tạo nền draw của ứng dụng để preview và xuất ra video
  3. Vẽ ảnh, text và tạo hiệu ứng của chúng
  4. Thêm music bên ngoài vào video
  5. Effect cho video
  6. 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 width, 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, width, 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 width, 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, width, 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,
                    width, 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

  1. Tạo frame recorder cho việc export hoặc preview cho video
  2. 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