airbnb / lottie-android

Render After Effects animations natively on Android and iOS, Web, and React Native
http://airbnb.io/lottie/
Apache License 2.0
34.96k stars 5.4k forks source link

How to save Lottie Animation as Video (.mp4) and GIF (.gif) in Android ? #1273

Closed axitasavani closed 5 years ago

axitasavani commented 5 years ago

is that another way to save Lottie animation in android?

i have sued the below code but video proper not save.


 private static final String MIME_TYPE = "video/avc";
    private static final int WIDTH = 640;
    private static final int HEIGHT = 640;
    private static final int BIT_RATE = 40000;
    private static final int FRAMES_PER_SECOND = 1;
    private static final int IFRAME_INTERVAL = 5;

    private static final int NUM_FRAMES = 8;

    // "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;
    LottieDrawable drawable = new LottieDrawable();

try {
            LottieTask<LottieComposition> composition = LottieCompositionFactory
                    .fromAsset(this, "text.json")
                    .addListener(new LottieListener<LottieComposition>() {
                        @Override
                        public void onResult(LottieComposition result) {
                            drawable.setComposition(result);
                        }
                    });
        } catch (Exception e) {
            Log.e(TAG, "onCreate: ", e);
        }

// Be VERY BAD and do the whole thing during onCreate().
                Log.i(TAG, "Generating movie...");
                String str = FileUtils.getSaveVideoDirPath();
                Log.i(TAG, "onClick: " + str);

                if (new File(str).exists()) {
                    Log.i(TAG, "onClick: EXISTS");
                    try {
                        File file = new File(str, "Demo_123.mp4");
                        Log.i(TAG, "onClick: " + file.getAbsolutePath());
                        generateMovie(file);
                        textUser.setText("Success");
                        Log.i(TAG, "Movie generation complete");
                    } catch (Exception ex) {
                        Log.e(TAG, "Movie generation FAILED", ex);
                        textUser.setText("Failed");
                    }
                }

private void generateMovie(File outputFile) {
        try {
            prepareEncoder(outputFile);

            for (int i = 0; i < drawable.getMaxFrame(); i++) {
                drainEncoder(false);
                Log.i(TAG, "generateMovie: " + i);
                drawable.setFrame(i);
                generateFrame(drawable);

            }

            drainEncoder(true);
        } catch (IOException ioe) {
            throw new RuntimeException(ioe);
        } finally {
            releaseEncoder();
        }
    }

    public void generateFrame(Drawable lottieDrawable) {
        drainEncoder(false);
        final Canvas canvas = mInputSurface.lockCanvas(null);
        try {
            lottieDrawable.draw(canvas);
        } finally {
            mInputSurface.unlockCanvasAndPost(canvas);
        }
    }

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

        MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, WIDTH, 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,
                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(TAG, "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);
        if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) {
            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(TAG, "output will go to " + outputFile);
        if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) {
            mMuxer = new MediaMuxer(outputFile.toString(),
                    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(TAG, "releasing encoder objects");
        if (mEncoder != null) {
            mEncoder.stop();
            mEncoder.release();
            mEncoder = null;
        }
        if (mInputSurface != null) {
            mInputSurface.release();
            mInputSurface = null;
        }
        if (mMuxer != null) {
            if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) {
                mMuxer.stop();
                mMuxer.release();
            }
            mMuxer = null;
        }
    }

    /**
     * Extracts all pending data from the encoder.
     * <p>
     * If endOfStream is not set, this returns when there is no more data to drain.  If it
     * is set, we send EOS to the encoder, and then iterate until we see EOS on the output.
     * Calling this with endOfStream set should be done once, right before stopping the muxer.
     */
    private void drainEncoder(boolean endOfStream) {
        final int TIMEOUT_USEC = 10000;
        if (VERBOSE) Log.d(TAG, "drainEncoder(" + endOfStream + ")");

        if (endOfStream) {
            if (VERBOSE) Log.d(TAG, "sending EOS to encoder");
            if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) {
                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(TAG, "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(TAG, "encoder output format changed: " + newFormat);

                // now that we have the Magic Goodies, start the muxer
                if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) {
                    mTrackIndex = mMuxer.addTrack(newFormat);
                    mMuxer.start();
                }

                mMuxerStarted = true;
            } else if (encoderStatus < 0) {
                Log.w(TAG, "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(TAG, "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;

                    if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) {
                        mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo);
                    }
                    if (VERBOSE) Log.d(TAG, "sent " + mBufferInfo.size + " bytes to muxer");
                }

                mEncoder.releaseOutputBuffer(encoderStatus, false);

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

@axitasavani The way you are using a LottieDrawable to draw to a canvas seems reasonable to me. Unfortunately, I can't provide generalized support here but drawing Lottie to a canvas should work the same as any other view.

gpeal commented 5 years ago

Also, duplicate of https://github.com/airbnb/lottie-android/issues/731

rkcoders commented 5 years ago

@axitasavani @gpeal hello i found solution of lottie animation convert to mp4 step 1: load animation in LottieAnimationView Step 2 : get duration of LottieAnimationView after set resorce Example: LottieAnimationView.getDuration(); step 3: LottieAnimationView.setDrawingCacheEnabled(true); step 4: use value animator and put duration of animation

Example: i = 0; ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f).setDuration(duration); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { Log.e("progress", "progress"); animationView.setProgress((Float) animation.getAnimatedValue()); //here you can get bitmap of every time; Bitmap bitmap = animationView.getDrawingCache(); StaticUtils.saveBitmap(MainActivity.this, bitmap, i); i++; } }); animator.start(); animator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) {

        }

        @Override
        public void onAnimationEnd(Animator animation) {
            new LongOperation().execute();
        }

        @Override
        public void onAnimationCancel(Animator animation) {

        }

        @Override
        public void onAnimationRepeat(Animator animation) {

        }
    });

step 5:you can see in above example onAnimationUpdate method using getDrawingCache method get bitmap and save in one folder

step 6:now you have multiple images there you can just make video from image using ffmpeg and generate mp4 file

thank you

axitasavani commented 5 years ago

Thank you. I'll apply this solution to save my Animation into Video. @rkcoders

abkoradiya commented 3 years ago

@axitasavani @gpeal hello i found solution of lottie animation convert to mp4 step 1: load animation in LottieAnimationView Step 2 : get duration of LottieAnimationView after set resorce Example: LottieAnimationView.getDuration(); step 3: LottieAnimationView.setDrawingCacheEnabled(true); step 4: use value animator and put duration of animation

Example: i = 0; ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f).setDuration(duration); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @override public void onAnimationUpdate(ValueAnimator animation) { Log.e("progress", "progress"); animationView.setProgress((Float) animation.getAnimatedValue()); //here you can get bitmap of every time; Bitmap bitmap = animationView.getDrawingCache(); StaticUtils.saveBitmap(MainActivity.this, bitmap, i); i++; } }); animator.start(); animator.addListener(new Animator.AnimatorListener() { @override public void onAnimationStart(Animator animation) {

        }

        @Override
        public void onAnimationEnd(Animator animation) {
            new LongOperation().execute();
        }

        @Override
        public void onAnimationCancel(Animator animation) {

        }

        @Override
        public void onAnimationRepeat(Animator animation) {

        }
    });

step 5:you can see in above example onAnimationUpdate method using getDrawingCache method get bitmap and save in one folder

step 6:now you have multiple images there you can just make video from image using ffmpeg and generate mp4 file

thank you

Hi @rkcoders can you reply me what is a ffmpeg command for making video from all frame images?

youssefhassan commented 3 years ago

@rkcoders This works only when the animation is shown on the screen, right? Do you know how to simulate that as for me it's static images still

Khenisiddharth commented 3 years ago

I didn't get Bitmap Image

krish-ktm commented 2 years ago

With images not working

KevinShingala commented 2 years ago

we can use in flutter

TannaInfo commented 1 year ago

is that another way to save Lottie animation in android?

i have sued the below code but video proper not save.


 private static final String MIME_TYPE = "video/avc";
    private static final int WIDTH = 640;
    private static final int HEIGHT = 640;
    private static final int BIT_RATE = 40000;
    private static final int FRAMES_PER_SECOND = 1;
    private static final int IFRAME_INTERVAL = 5;

    private static final int NUM_FRAMES = 8;

    // "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;
    LottieDrawable drawable = new LottieDrawable();

try {
            LottieTask<LottieComposition> composition = LottieCompositionFactory
                    .fromAsset(this, "text.json")
                    .addListener(new LottieListener<LottieComposition>() {
                        @Override
                        public void onResult(LottieComposition result) {
                            drawable.setComposition(result);
                        }
                    });
        } catch (Exception e) {
            Log.e(TAG, "onCreate: ", e);
        }

// Be VERY BAD and do the whole thing during onCreate().
                Log.i(TAG, "Generating movie...");
                String str = FileUtils.getSaveVideoDirPath();
                Log.i(TAG, "onClick: " + str);

                if (new File(str).exists()) {
                    Log.i(TAG, "onClick: EXISTS");
                    try {
                        File file = new File(str, "Demo_123.mp4");
                        Log.i(TAG, "onClick: " + file.getAbsolutePath());
                        generateMovie(file);
                        textUser.setText("Success");
                        Log.i(TAG, "Movie generation complete");
                    } catch (Exception ex) {
                        Log.e(TAG, "Movie generation FAILED", ex);
                        textUser.setText("Failed");
                    }
                }

private void generateMovie(File outputFile) {
        try {
            prepareEncoder(outputFile);

            for (int i = 0; i < drawable.getMaxFrame(); i++) {
                drainEncoder(false);
                Log.i(TAG, "generateMovie: " + i);
                drawable.setFrame(i);
                generateFrame(drawable);

            }

            drainEncoder(true);
        } catch (IOException ioe) {
            throw new RuntimeException(ioe);
        } finally {
            releaseEncoder();
        }
    }

    public void generateFrame(Drawable lottieDrawable) {
        drainEncoder(false);
        final Canvas canvas = mInputSurface.lockCanvas(null);
        try {
            lottieDrawable.draw(canvas);
        } finally {
            mInputSurface.unlockCanvasAndPost(canvas);
        }
    }

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

        MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, WIDTH, 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,
                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(TAG, "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);
        if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) {
            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(TAG, "output will go to " + outputFile);
        if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) {
            mMuxer = new MediaMuxer(outputFile.toString(),
                    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(TAG, "releasing encoder objects");
        if (mEncoder != null) {
            mEncoder.stop();
            mEncoder.release();
            mEncoder = null;
        }
        if (mInputSurface != null) {
            mInputSurface.release();
            mInputSurface = null;
        }
        if (mMuxer != null) {
            if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) {
                mMuxer.stop();
                mMuxer.release();
            }
            mMuxer = null;
        }
    }

    /**
     * Extracts all pending data from the encoder.
     * <p>
     * If endOfStream is not set, this returns when there is no more data to drain.  If it
     * is set, we send EOS to the encoder, and then iterate until we see EOS on the output.
     * Calling this with endOfStream set should be done once, right before stopping the muxer.
     */
    private void drainEncoder(boolean endOfStream) {
        final int TIMEOUT_USEC = 10000;
        if (VERBOSE) Log.d(TAG, "drainEncoder(" + endOfStream + ")");

        if (endOfStream) {
            if (VERBOSE) Log.d(TAG, "sending EOS to encoder");
            if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) {
                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(TAG, "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(TAG, "encoder output format changed: " + newFormat);

                // now that we have the Magic Goodies, start the muxer
                if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) {
                    mTrackIndex = mMuxer.addTrack(newFormat);
                    mMuxer.start();
                }

                mMuxerStarted = true;
            } else if (encoderStatus < 0) {
                Log.w(TAG, "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(TAG, "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;

                    if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) {
                        mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo);
                    }
                    if (VERBOSE) Log.d(TAG, "sent " + mBufferInfo.size + " bytes to muxer");
                }

                mEncoder.releaseOutputBuffer(encoderStatus, false);

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

video save but open video to show can't play video.