Tìm hiểu về movie Maker trên android - Phần 2

Tiếp tục ở phần 1 Ở phần này mình sẽ giới thiệu tạo ra video đơn giản nhất và viết demo hướng dẫn Mục tiêu

  • Tạo ra video demo với zoom hình ảnh đầu vào
  • Add audio sau khi có video
  • Xuất ra video cuối cùng với thời lượng bằng thời lượng video tạo ra ban đầu (cho dù audio add vào có độ dài hơn)

Chi tiết

Như hướng dẫn ở bài trước mình tạo ra lớp GeneratedMovie bây giờ mình sẽ tổng hợp nó lại thành 1 class VideoUtils mục đích tạo ra video với 1 số thuộc tính đầu vào (zoom hình ảnh)

public class VideoUtils {
    private static final boolean VERBOSE = false;
    /**
     * Thuộc tính của file xuất video/avc dạng video
     **/
    private static final String MIME_TYPE = "video/avc";

    /**
     * Bitrate của video
     **/
    private static final int BIT_RATE = 2000000;
    /**
     * Số lượng frame trên 1 giây, càng lớn sẽ càng mượt nhưng mắt thường khó cảm nhận được hết, với các video HD  hiện tại trên Youtube đang ở mức 28
     **/
    private static final int FRAMES_PER_SECOND = 30;
    private static final int IFRAME_INTERVAL = 5;

    /** Khai báo width, height của video, các bạn có thể thay đổi thành video HD tuỳ ý muốn **/

    private static final int VIDEO_WIDTH = 640;
    private static final int VIDEO_HEIGHT = 480;

    // "live" state during recording
    private MediaCodec.BufferInfo mBufferInfo;
    private MediaCodec mEncoder;
    private MediaMuxer mMuxer;
    private Surface mInputSurface;
    private int mTrackIndex;
    private boolean mMuxerStarted;
    private long mFakePts;

    private Context context;
    private File output;
    private Bitmap bitmap;
    /**
     * Vì ở đây ta sử dụng bitmap để vẽ, vì vậy cần có thuộc tính FILTER_BITMAP_FLAG để khi zoom ảnh không bị vỡ hoặc nhoè ảnh
     **/
    private Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);

    /**
     * Có thể định nghĩa trước số frame, ví dụ muốn tạo video 5giây, số frame = FRAMES_PER_SECOND * 5;
     **/
    private int maxFrame;
    private float currentZoom = 1.0f;

    public VideoUtils(Context context) {
        this.context = context;

        try {
            output = new File(context.getFilesDir(), "tmp.mp4");
            prepareEncoder(output);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void prepare() {
        /** Load hình ảnh lên cho việc zoom ảnh **/
        bitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher);
        currentZoom = 1.0f;
    }

    public String makeVideo() {
        try {
            /** Tạo ra video có thời lượng là 5giây **/
            maxFrame = 150;
            for (int i = 0; i < maxFrame; i++) {
                // chuẩn bị cho việc vẽ lên surface
                drainEncoder(false);
                // Tạo ra từng frame trên surface
                generateFrame(i);

                /** Tính toán percent exported, để có thể đưa ra dialog thông báo cho người dùng, cho họ biết còn cần phải chờ bao lâu nữa **/
                float percent = 100.0f * i / (float) maxFrame;
            }

            drainEncoder(true);
        } finally {
            releaseEncoder();
        }

        return output.getAbsolutePath();
    }

    /**
     * Prepares the video encoder, muxer, and an input surface.
     */
    private void prepareEncoder(File outputFile) throws IOException {
        mBufferInfo = new MediaCodec.BufferInfo();

        MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, VIDEO_WIDTH, VIDEO_HEIGHT);

        // Set some properties.  Failing to specify some of these can cause the MediaCodec
        // configure() call to throw an unhelpful exception.
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
                MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
        format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAMES_PER_SECOND);
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
        if (VERBOSE) Log.d("format: " + format);

        // Create a MediaCodec encoder, and configure it with our format.  Get a Surface
        // we can use for input and wrap it with a class that handles the EGL work.
        mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
        mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        mInputSurface = mEncoder.createInputSurface();
        mEncoder.start();

        // Create a MediaMuxer.  We can't add the video track and start() the muxer here,
        // because our MediaFormat doesn't have the Magic Goodies.  These can only be
        // obtained from the encoder after it has started processing data.
        //
        // We're not actually interested in multiplexing audio.  We just want to convert
        // the raw H.264 elementary stream we get from MediaCodec into a .mp4 file.
        if (VERBOSE) Log.d("output will go to " + outputFile);
        mMuxer = new MediaMuxer(outputFile.toString(),
                MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

        mTrackIndex = -1;
        mMuxerStarted = false;
    }

    /**
     * Releases encoder resources.  May be called after partial / failed initialization.
     */
    private void releaseEncoder() {
        if (VERBOSE) Log.d("releasing encoder objects");
        if (mEncoder != null) {
            mEncoder.stop();
            mEncoder.release();
            mEncoder = null;
        }
        if (mInputSurface != null) {
            mInputSurface.release();
            mInputSurface = null;
        }
        if (mMuxer != null) {
            mMuxer.stop();
            mMuxer.release();
            mMuxer = null;
        }
    }

    private void drainEncoder(boolean endOfStream) {
        /** Thời gian delay giữa 2 frame **/
        final int TIMEOUT_USEC = 2500;
        if (VERBOSE) Log.d("drainEncoder(" + endOfStream + ")");

        if (endOfStream) {
            if (VERBOSE) Log.d("sending EOS to encoder");
            mEncoder.signalEndOfInputStream();
        }

        ByteBuffer[] encoderOutputBuffers = mEncoder.getOutputBuffers();
        while (true) {
            int encoderStatus = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
            if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
                // no output available yet
                if (!endOfStream) {
                    break;      // out of while
                } else {
                    if (VERBOSE) Log.d("no output available, spinning to await EOS");
                }
            } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                // not expected for an encoder
                encoderOutputBuffers = mEncoder.getOutputBuffers();
            } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                // should happen before receiving buffers, and should only happen once
                if (mMuxerStarted) {
                    throw new RuntimeException("format changed twice");
                }
                MediaFormat newFormat = mEncoder.getOutputFormat();
                Log.d("encoder output format changed: " + newFormat);

                // now that we have the Magic Goodies, start the muxer
                mTrackIndex = mMuxer.addTrack(newFormat);
                mMuxer.start();
                mMuxerStarted = true;
            } else if (encoderStatus < 0) {
                Log.w("unexpected result from encoder.dequeueOutputBuffer: " +
                        encoderStatus);
                // let's ignore it
            } else {
                ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
                if (encodedData == null) {
                    throw new RuntimeException("encoderOutputBuffer " + encoderStatus +
                            " was null");
                }

                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                    // The codec config data was pulled out and fed to the muxer when we got
                    // the INFO_OUTPUT_FORMAT_CHANGED status.  Ignore it.
                    if (VERBOSE) Log.d("ignoring BUFFER_FLAG_CODEC_CONFIG");
                    mBufferInfo.size = 0;
                }

                if (mBufferInfo.size != 0) {
                    if (!mMuxerStarted) {
                        throw new RuntimeException("muxer hasn't started");
                    }

                    // adjust the ByteBuffer values to match BufferInfo
                    encodedData.position(mBufferInfo.offset);
                    encodedData.limit(mBufferInfo.offset + mBufferInfo.size);
                    mBufferInfo.presentationTimeUs = mFakePts;
                    mFakePts += 1000000L / FRAMES_PER_SECOND;

                    mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo);
                    if (VERBOSE) Log.d("sent " + mBufferInfo.size + " bytes to muxer");
                }

                mEncoder.releaseOutputBuffer(encoderStatus, false);

                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    if (!endOfStream) {
                        Log.w("reached end of stream unexpectedly");
                    } else {
                        if (VERBOSE) Log.d("end of stream reached");
                    }
                    break;      // out of while
                }
            }
        }
    }

    /**
     * Vẽ từng frame theo thời gian, ví dụ ở giây 1 vẽ ảnh với zoom 1.1f, giây 2 vẽ ảnh zoom với 1.2f
     **/
    private void generateFrame(int frameNum) {
        /** Khởi tạo canvas để vẽ từng frame cho video **/
        Canvas canvas = mInputSurface.lockCanvas(null);
        try {
            /** Trong 5 giây ta sẽ vẽ hình ảnh zoom từ 1.0 -> 1.3 **/
            /** Như vậy ta sẽ phải tính toán trong thời gian thứ i hình ảnh đang zoom ở mức bao nhiêu **/
            long currentDuration = computePresentationTimeNsec(frameNum);
            float currentZoom = 1.0f + currentDuration * (1.3f - 1.0f) / 5000.0f;

            Matrix matrix = new Matrix();
            matrix.setScale(currentZoom, currentZoom);
            canvas.drawBitmap(bitmap, matrix, paint);

        } finally {
            mInputSurface.unlockCanvasAndPost(canvas);
        }
    }

    /**
     * Vì thời gian ở đây được tính toán dựa trên nanosecond, nên ta sẽ tính toán thời gian của video khi vẽ ở frame thứ i, thời gian video được tính toán ra milisecond
     **/
    private static long computePresentationTimeNsec(int frameIndex) {
        final long ONE_BILLION = 1000000000;
        return frameIndex * ONE_BILLION / FRAMES_PER_SECOND;
    }
}

Như vậy ta đã viết xong class tạo ra video, video xuất ra sẽ lưu ở trong thư mục của ứng dụng, bây giờ ta sẽ viết thêm thuộc tính add audio khi đã có video base

** AddAudio** mình sử dụng lib đã viết sẵn của MP4Parser gồm 2 lib

  • isoparser-1.0.2.jar
  • aspectjrt-1.7.3.jar

Lưu ý, với video đầu ra dạng mp4, nên audio đầu vào phải được định nghĩa dạng MPEG4: acc, m4a,...

Như vậy các công việc khi add audio ta cần làm

  • Crop audio nếu dài hơn video
  • Add Video và audio vào tracker
private String addAudio(String videoSource, String audioSource, String outputFile) throws IOException {
        File output = new File(outputFile);

        // Load video đầu vào
        Movie originalMovie = MovieCreator.build(videoSource);
        Movie audio = MovieCreator.build(new FileDataSourceImpl(new File(audioSource)));
        //get duration of video
        IsoFile isoFile = new IsoFile(videoSource);
        double lengthInSeconds = (double)
                isoFile.getMovieBox().getMovieHeaderBox().getDuration() /
                isoFile.getMovieBox().getMovieHeaderBox().getTimescale();

        Track track = originalMovie.getTracks().get(0);
        Track audioTrack = audio.getTracks().get(0);

        double startTime1 = 0;
        double endTime1 = lengthInSeconds;

        if (audioTrack.getSyncSamples() != null && audioTrack.getSyncSamples().length > 0) {
            startTime1 = correctTimeToSyncSample(audioTrack, startTime1, false);
            endTime1 = correctTimeToSyncSample(audioTrack, endTime1, true);
        }

        long currentSample = 0;
        double currentTime = 0;
        double lastTime = -1;
        long startSample1 = -1;
        long endSample1 = -1;

        for (int i = 0; i < audioTrack.getSampleDurations().length; i++) {
            long delta = audioTrack.getSampleDurations()[i];

            if (currentTime > lastTime && currentTime <= startTime1) {
                // current sample is still before the new start time
                startSample1 = currentSample;
            }
            if (currentTime > lastTime && currentTime <= endTime1) {
                // current sample is after the new start time and still before the new end time
                endSample1 = currentSample;
            }

            lastTime = currentTime;
            currentTime += (double) delta / (double) audioTrack.getTrackMetaData().getTimescale();
            currentSample++;
        }

        // Crop audio với độ lớn phù hợp với video
        CroppedTrack cropperAacTrack = new CroppedTrack(audioTrack, startSample1, endSample1);

        Movie movie = new Movie();

        movie.addTrack(track);
        // Add audio vào video
        movie.addTrack(cropperAacTrack);

        Container mp4file = new DefaultMp4Builder().build(movie);

        FileChannel fc = new FileOutputStream(output).getChannel();
        mp4file.writeContainer(fc);
        fc.close();

        return output.getAbsolutePath();
    }

    private static double correctTimeToSyncSample(Track track, double cutHere, boolean next) {
        double[] timeOfSyncSamples = new double[track.getSyncSamples().length];
        long currentSample = 0;
        double currentTime = 0;
        for (int i = 0; i < track.getSampleDurations().length; i++) {
            long delta = track.getSampleDurations()[i];

            if (Arrays.binarySearch(track.getSyncSamples(), currentSample + 1) >= 0) {
                // samples always start with 1 but we start with zero therefore +1
                timeOfSyncSamples[Arrays.binarySearch(track.getSyncSamples(), currentSample + 1)] = currentTime;
            }
            currentTime += (double) delta / (double) track.getTrackMetaData().getTimescale();
            currentSample++;

        }
        double previous = 0;
        for (double timeOfSyncSample : timeOfSyncSamples) {
            if (timeOfSyncSample > cutHere) {
                if (next) {
                    return timeOfSyncSample;
                } else {
                    return previous;
                }
            }
            previous = timeOfSyncSample;
        }
        return timeOfSyncSamples[timeOfSyncSamples.length - 1];
    }

Như vậy ta đã xong hàm tạo video và thuộc tính add adio Bây giờ ta chỉ cần có 1 file m4a có sẵn trong sdcard là ta đã có thể tạo ra 1 video với hình ảnh + audio đầu vào rồi

Dưới đây là project sample với full source trên mình đã viết, các bạn có thể tải về và tham khảo

Với demo này chỉ có duy nhất 1 hiệu ứng zoom, ở bài sau mình sẽ hướng dẫn với các hiệu ứng cao cấp hơn: translate, translate theo trục Z (2.5D), một số hiệu ứng khác giốgn như các ứng dụng trên máy tính

"Code Sample":https://www.dropbox.com/s/rgghsvbp8t1uzjy/MovieMakerSample.zip?dl=0