| /* |
| * Copyright 2014 Google Inc. |
| * |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #include "experimental/ffmpeg/SkVideoEncoder.h" |
| #include "include/core/SkCanvas.h" |
| #include "include/core/SkGraphics.h" |
| #include "include/core/SkStream.h" |
| #include "include/core/SkSurface.h" |
| #include "include/core/SkTime.h" |
| #include "include/private/SkTPin.h" |
| #include "modules/skottie/include/Skottie.h" |
| #include "modules/skresources/include/SkResources.h" |
| #include "src/utils/SkOSPath.h" |
| |
| #include "tools/flags/CommandLineFlags.h" |
| #include "tools/gpu/GrContextFactory.h" |
| |
| #include "include/gpu/GrContextOptions.h" |
| |
| static DEFINE_string2(input, i, "", "skottie animation to render"); |
| static DEFINE_string2(output, o, "", "mp4 file to create"); |
| static DEFINE_string2(assetPath, a, "", "path to assets needed for json file"); |
| static DEFINE_int_2(fps, f, 25, "fps"); |
| static DEFINE_bool2(verbose, v, false, "verbose mode"); |
| static DEFINE_bool2(loop, l, false, "loop mode for profiling"); |
| static DEFINE_int(set_dst_width, 0, "set destination width (height will be computed)"); |
| static DEFINE_bool2(gpu, g, false, "use GPU for rendering"); |
| |
| static void produce_frame(SkSurface* surf, skottie::Animation* anim, double frame) { |
| anim->seekFrame(frame); |
| surf->getCanvas()->clear(SK_ColorWHITE); |
| anim->render(surf->getCanvas()); |
| } |
| |
| struct AsyncRec { |
| SkImageInfo info; |
| SkVideoEncoder* encoder; |
| }; |
| |
| int main(int argc, char** argv) { |
| SkGraphics::Init(); |
| |
| CommandLineFlags::SetUsage("Converts skottie to a mp4"); |
| CommandLineFlags::Parse(argc, argv); |
| |
| if (FLAGS_input.count() == 0) { |
| SkDebugf("-i input_file.json argument required\n"); |
| return -1; |
| } |
| |
| auto contextType = sk_gpu_test::GrContextFactory::kGL_ContextType; |
| GrContextOptions grCtxOptions; |
| sk_gpu_test::GrContextFactory factory(grCtxOptions); |
| |
| SkString assetPath; |
| if (FLAGS_assetPath.count() > 0) { |
| assetPath.set(FLAGS_assetPath[0]); |
| } else { |
| assetPath = SkOSPath::Dirname(FLAGS_input[0]); |
| } |
| SkDebugf("assetPath %s\n", assetPath.c_str()); |
| |
| auto animation = skottie::Animation::Builder() |
| .setResourceProvider(skresources::FileResourceProvider::Make(assetPath)) |
| .makeFromFile(FLAGS_input[0]); |
| if (!animation) { |
| SkDebugf("failed to load %s\n", FLAGS_input[0]); |
| return -1; |
| } |
| |
| SkISize dim = animation->size().toRound(); |
| double duration = animation->duration(); |
| int fps = SkTPin(FLAGS_fps, 1, 240); |
| double fps_scale = animation->fps() / fps; |
| |
| float scale = 1; |
| if (FLAGS_set_dst_width > 0) { |
| scale = FLAGS_set_dst_width / (float)dim.width(); |
| dim = { FLAGS_set_dst_width, SkScalarRoundToInt(scale * dim.height()) }; |
| } |
| |
| const int frames = SkScalarRoundToInt(duration * fps); |
| const double frame_duration = 1.0 / fps; |
| |
| if (FLAGS_verbose) { |
| SkDebugf("Size %dx%d duration %g, fps %d, frame_duration %g\n", |
| dim.width(), dim.height(), duration, fps, frame_duration); |
| } |
| |
| SkVideoEncoder encoder; |
| |
| GrDirectContext* context = nullptr; |
| sk_sp<SkSurface> surf; |
| sk_sp<SkData> data; |
| |
| const auto info = SkImageInfo::MakeN32Premul(dim); |
| do { |
| double loop_start = SkTime::GetSecs(); |
| |
| if (!encoder.beginRecording(dim, fps)) { |
| SkDEBUGF("Invalid video stream configuration.\n"); |
| return -1; |
| } |
| |
| // lazily allocate the surfaces |
| if (!surf) { |
| if (FLAGS_gpu) { |
| context = factory.getContextInfo(contextType).directContext(); |
| surf = SkSurface::MakeRenderTarget(context, |
| SkBudgeted::kNo, |
| info, |
| 0, |
| GrSurfaceOrigin::kTopLeft_GrSurfaceOrigin, |
| nullptr); |
| if (!surf) { |
| context = nullptr; |
| } |
| } |
| if (!surf) { |
| surf = SkSurface::MakeRaster(info); |
| } |
| surf->getCanvas()->scale(scale, scale); |
| } |
| |
| for (int i = 0; i <= frames; ++i) { |
| const double frame = i * fps_scale; |
| if (FLAGS_verbose) { |
| SkDebugf("rendering frame %g\n", frame); |
| } |
| |
| produce_frame(surf.get(), animation.get(), frame); |
| |
| AsyncRec asyncRec = { info, &encoder }; |
| if (context) { |
| auto read_pixels_cb = [](SkSurface::ReadPixelsContext ctx, |
| std::unique_ptr<const SkSurface::AsyncReadResult> result) { |
| if (result && result->count() == 1) { |
| AsyncRec* rec = reinterpret_cast<AsyncRec*>(ctx); |
| rec->encoder->addFrame({rec->info, result->data(0), result->rowBytes(0)}); |
| } |
| }; |
| surf->asyncRescaleAndReadPixels(info, {0, 0, info.width(), info.height()}, |
| SkSurface::RescaleGamma::kSrc, |
| kNone_SkFilterQuality, |
| read_pixels_cb, &asyncRec); |
| context->submit(); |
| } else { |
| SkPixmap pm; |
| SkAssertResult(surf->peekPixels(&pm)); |
| encoder.addFrame(pm); |
| } |
| } |
| data = encoder.endRecording(); |
| |
| if (FLAGS_loop) { |
| double loop_dur = SkTime::GetSecs() - loop_start; |
| SkDebugf("recording secs %g, frames %d, recording fps %d\n", |
| loop_dur, frames, (int)(frames / loop_dur)); |
| } |
| } while (FLAGS_loop); |
| |
| if (FLAGS_output.count() == 0) { |
| SkDebugf("missing -o output_file.mp4 argument\n"); |
| return 0; |
| } |
| |
| SkFILEWStream ostream(FLAGS_output[0]); |
| if (!ostream.isValid()) { |
| SkDebugf("Can't create output file %s\n", FLAGS_output[0]); |
| return -1; |
| } |
| ostream.write(data->data(), data->size()); |
| return 0; |
| } |