/* * Copyright 2018 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "include/core/SkCanvas.h" #include "include/core/SkGraphics.h" #include "include/core/SkPictureRecorder.h" #include "include/core/SkStream.h" #include "include/core/SkSurface.h" #include "include/encode/SkPngEncoder.h" #include "include/private/SkTPin.h" #include "modules/skottie/include/Skottie.h" #include "modules/skottie/utils/SkottieUtils.h" #include "modules/skresources/include/SkResources.h" #include "src/core/SkOSFile.h" #include "src/core/SkTaskGroup.h" #include "src/utils/SkOSPath.h" #include "tools/flags/CommandLineFlags.h" #include #include #include #include #include #if defined(HAVE_VIDEO_ENCODER) #include "experimental/ffmpeg/SkVideoEncoder.h" const char* formats_help = "Output format (png, skp, mp4, or null)"; #else const char* formats_help = "Output format (png, skp, or null)"; #endif static DEFINE_string2(input , i, nullptr, "Input .json file."); static DEFINE_string2(writePath, w, nullptr, "Output directory. Frames are names [0-9]{6}.png."); static DEFINE_string2(format , f, "png" , formats_help); static DEFINE_double(t0, 0, "Timeline start [0..1]."); static DEFINE_double(t1, 1, "Timeline stop [0..1]."); static DEFINE_double(fps, 0, "Decode frames per second (default is animation native fps)."); static DEFINE_int(width , 800, "Render width."); static DEFINE_int(height, 600, "Render height."); static DEFINE_int(threads, 0, "Number of worker threads (0 -> cores count)."); namespace { static constexpr SkColor kClearColor = SK_ColorWHITE; std::unique_ptr MakeFrameStream(size_t idx, const char* ext) { const auto frame_file = SkStringPrintf("0%06zu.%s", idx, ext); auto stream = std::make_unique(SkOSPath::Join(FLAGS_writePath[0], frame_file.c_str()).c_str()); if (!stream->isValid()) { return nullptr; } return stream; } class Sink { public: Sink() = default; virtual ~Sink() = default; Sink(const Sink&) = delete; Sink& operator=(const Sink&) = delete; virtual SkCanvas* beginFrame(size_t idx) = 0; virtual bool endFrame(size_t idx) = 0; }; class PNGSink final : public Sink { public: static std::unique_ptr Make(const SkMatrix& scale_matrix) { auto surface = SkSurface::MakeRasterN32Premul(FLAGS_width, FLAGS_height); if (!surface) { SkDebugf("Could not allocate a %d x %d surface.\n", FLAGS_width, FLAGS_height); return nullptr; } return std::unique_ptr(new PNGSink(std::move(surface), scale_matrix)); } private: PNGSink(sk_sp surface, const SkMatrix& scale_matrix) : fSurface(std::move(surface)) { fSurface->getCanvas()->concat(scale_matrix); } SkCanvas* beginFrame(size_t) override { auto* canvas = fSurface->getCanvas(); canvas->clear(kClearColor); return canvas; } bool endFrame(size_t idx) override { auto stream = MakeFrameStream(idx, "png"); if (!stream) { return false; } // Set encoding options to favor speed over size. SkPngEncoder::Options options; options.fZLibLevel = 1; options.fFilterFlags = SkPngEncoder::FilterFlag::kNone; sk_sp img = fSurface->makeImageSnapshot(); SkPixmap pixmap; return img->peekPixels(&pixmap) && SkPngEncoder::Encode(stream.get(), pixmap, options); } const sk_sp fSurface; }; class SKPSink final : public Sink { public: static std::unique_ptr Make(const SkMatrix& scale_matrix) { return std::unique_ptr(new SKPSink(scale_matrix)); } private: explicit SKPSink(const SkMatrix& scale_matrix) : fScaleMatrix(scale_matrix) {} SkCanvas* beginFrame(size_t) override { auto canvas = fRecorder.beginRecording(FLAGS_width, FLAGS_height); canvas->concat(fScaleMatrix); return canvas; } bool endFrame(size_t idx) override { auto stream = MakeFrameStream(idx, "skp"); if (!stream) { return false; } fRecorder.finishRecordingAsPicture()->serialize(stream.get()); return true; } const SkMatrix fScaleMatrix; SkPictureRecorder fRecorder; }; class NullSink final : public Sink { public: static std::unique_ptr Make(const SkMatrix& scale_matrix) { auto surface = SkSurface::MakeRasterN32Premul(FLAGS_width, FLAGS_height); if (!surface) { SkDebugf("Could not allocate a %d x %d surface.\n", FLAGS_width, FLAGS_height); return nullptr; } return std::unique_ptr(new NullSink(std::move(surface), scale_matrix)); } private: NullSink(sk_sp surface, const SkMatrix& scale_matrix) : fSurface(std::move(surface)) { fSurface->getCanvas()->concat(scale_matrix); } SkCanvas* beginFrame(size_t) override { auto* canvas = fSurface->getCanvas(); canvas->clear(kClearColor); return canvas; } bool endFrame(size_t) override { return true; } const sk_sp fSurface; }; static std::vector>> gMP4Frames; struct MP4Sink final : public Sink { explicit MP4Sink(const SkMatrix& scale_matrix) : fSurface(SkSurface::MakeRasterN32Premul(FLAGS_width, FLAGS_height)) { fSurface->getCanvas()->concat(scale_matrix); } SkCanvas* beginFrame(size_t) override { SkCanvas* canvas = fSurface->getCanvas(); canvas->clear(kClearColor); return canvas; } bool endFrame(size_t i) override { if (sk_sp img = fSurface->makeImageSnapshot()) { gMP4Frames[i].set_value(std::move(img)); return true; } return false; } const sk_sp fSurface; }; class Logger final : public skottie::Logger { public: struct LogEntry { SkString fMessage, fJSON; }; void log(skottie::Logger::Level lvl, const char message[], const char json[]) override { auto& log = lvl == skottie::Logger::Level::kError ? fErrors : fWarnings; log.push_back({ SkString(message), json ? SkString(json) : SkString() }); } void report() const { SkDebugf("Animation loaded with %zu error%s, %zu warning%s.\n", fErrors.size(), fErrors.size() == 1 ? "" : "s", fWarnings.size(), fWarnings.size() == 1 ? "" : "s"); const auto& show = [](const LogEntry& log, const char prefix[]) { SkDebugf("%s%s", prefix, log.fMessage.c_str()); if (!log.fJSON.isEmpty()) SkDebugf(" : %s", log.fJSON.c_str()); SkDebugf("\n"); }; for (const auto& err : fErrors) show(err, " !! "); for (const auto& wrn : fWarnings) show(wrn, " ?? "); } private: std::vector fErrors, fWarnings; }; std::unique_ptr MakeSink(const char* fmt, const SkMatrix& scale_matrix) { if (0 == strcmp(fmt, "png")) return PNGSink::Make(scale_matrix); if (0 == strcmp(fmt, "skp")) return SKPSink::Make(scale_matrix); if (0 == strcmp(fmt, "null")) return NullSink::Make(scale_matrix); if (0 == strcmp(fmt, "mp4")) return std::make_unique(scale_matrix); SkDebugf("Unknown format: %s\n", FLAGS_format[0]); return nullptr; } } // namespace extern bool gSkUseThreadLocalStrikeCaches_IAcknowledgeThisIsIncrediblyExperimental; int main(int argc, char** argv) { gSkUseThreadLocalStrikeCaches_IAcknowledgeThisIsIncrediblyExperimental = true; CommandLineFlags::Parse(argc, argv); SkAutoGraphics ag; if (FLAGS_input.isEmpty() || FLAGS_writePath.isEmpty()) { SkDebugf("Missing required 'input' and 'writePath' args.\n"); return 1; } if (!FLAGS_format.contains("mp4") && !sk_mkdir(FLAGS_writePath[0])) { return 1; } auto logger = sk_make_sp(); auto rp = skresources::CachingResourceProvider::Make( skresources::DataURIResourceProviderProxy::Make( skresources::FileResourceProvider::Make(SkOSPath::Dirname(FLAGS_input[0]), /*predecode=*/true), /*predecode=*/true)); auto data = SkData::MakeFromFileName(FLAGS_input[0]); auto precomp_interceptor = sk_make_sp(rp, "__"); if (!data) { SkDebugf("Could not load %s.\n", FLAGS_input[0]); return 1; } // Instantiate an animation on the main thread for two reasons: // - we need to know its duration upfront // - we want to only report parsing errors once auto anim = skottie::Animation::Builder() .setLogger(logger) .setResourceProvider(rp) .make(static_cast(data->data()), data->size()); if (!anim) { SkDebugf("Could not parse animation: '%s'.\n", FLAGS_input[0]); return 1; } const auto scale_matrix = SkMatrix::RectToRect(SkRect::MakeSize(anim->size()), SkRect::MakeIWH(FLAGS_width, FLAGS_height), SkMatrix::kCenter_ScaleToFit); logger->report(); const auto t0 = SkTPin(FLAGS_t0, 0.0, 1.0), t1 = SkTPin(FLAGS_t1, t0, 1.0), native_fps = anim->fps(), frame0 = anim->duration() * t0 * native_fps, duration = anim->duration() * (t1 - t0); double fps = FLAGS_fps > 0 ? FLAGS_fps : native_fps; if (fps <= 0) { SkDebugf("Invalid fps: %f.\n", fps); return 1; } auto frame_count = static_cast(duration * fps); static constexpr int kMaxFrames = 10000; if (frame_count > kMaxFrames) { frame_count = kMaxFrames; fps = frame_count / duration; } const auto fps_scale = native_fps / fps; SkDebugf("Rendering %f seconds (%d frames @%f fps).\n", duration, frame_count, fps); if (FLAGS_format.contains("mp4")) { gMP4Frames.resize(frame_count); } std::vector frames_ms(frame_count); auto ms_since = [](auto start) { const auto elapsed = std::chrono::steady_clock::now() - start; return std::chrono::duration_cast(elapsed).count(); }; SkTaskGroup::Enabler enabler(FLAGS_threads - 1); SkTaskGroup tg; tg.batch(frame_count, [&](int i) { // SkTaskGroup::Enabler creates a LIFO work pool, // but we want our early frames to start first. i = frame_count - 1 - i; const auto start = std::chrono::steady_clock::now(); thread_local static auto* anim = skottie::Animation::Builder() .setResourceProvider(rp) .setPrecompInterceptor(precomp_interceptor) .make(static_cast(data->data()), data->size()) .release(); thread_local static auto* sink = MakeSink(FLAGS_format[0], scale_matrix).release(); if (sink && anim) { anim->seekFrame(frame0 + i * fps_scale); anim->render(sink->beginFrame(i)); sink->endFrame(i); } frames_ms[i] = ms_since(start); }); #if defined(HAVE_VIDEO_ENCODER) if (FLAGS_format.contains("mp4")) { SkVideoEncoder enc; if (!enc.beginRecording({FLAGS_width, FLAGS_height}, fps)) { SkDEBUGF("Invalid video stream configuration.\n"); return -1; } std::vector starved_ms; for (std::promise>& frame : gMP4Frames) { const auto start = std::chrono::steady_clock::now(); sk_sp img = frame.get_future().get(); starved_ms.push_back(ms_since(start)); SkPixmap pm; SkAssertResult(img->peekPixels(&pm)); enc.addFrame(pm); } sk_sp mp4 = enc.endRecording(); SkFILEWStream{FLAGS_writePath[0]} .write(mp4->data(), mp4->size()); // If everything's going well, the first frame should account for the most, // and ideally nearly all, starvation. double first = starved_ms[0]; std::sort(starved_ms.begin(), starved_ms.end()); double sum = std::accumulate(starved_ms.begin(), starved_ms.end(), 0); SkDebugf("starved min %gms, med %gms, avg %gms, max %gms, sum %gms, first %gms (%s)\n", starved_ms[0], starved_ms[frame_count/2], sum/frame_count, starved_ms.back(), sum, first, first == starved_ms.back() ? "ok" : "BAD"); } #endif tg.wait(); std::sort(frames_ms.begin(), frames_ms.end()); double sum = std::accumulate(frames_ms.begin(), frames_ms.end(), 0); SkDebugf("frame time min %gms, med %gms, avg %gms, max %gms, sum %gms\n", frames_ms[0], frames_ms[frame_count/2], sum/frame_count, frames_ms.back(), sum); return 0; }