From 9afcb67630c7ef91cb4e00e2b51a00de04f276c1 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Thu, 5 Feb 2015 00:02:59 -0600 Subject: [PATCH] Added a new ImageWriter class, which specializes in creating images (powered by ImageMagick). This can quickly and easily create animated GIFs with nice color pallets, and a hundred other misc image formats. --- include/ImageWriter.h | 156 +++++++++++++++++++++++++ include/WriterBase.h | 17 ++- src/ImageWriter.cpp | 257 ++++++++++++++++++++++++++++++++++++++++++ src/WriterBase.cpp | 6 + 4 files changed, 432 insertions(+), 4 deletions(-) create mode 100644 include/ImageWriter.h create mode 100644 src/ImageWriter.cpp diff --git a/include/ImageWriter.h b/include/ImageWriter.h new file mode 100644 index 00000000..211e9f4b --- /dev/null +++ b/include/ImageWriter.h @@ -0,0 +1,156 @@ +/** + * @file + * @brief Header file for ImageWriter class + * @author Jonathan Thomas , Fabrice Bellard + * + * @section LICENSE + * + * Copyright (c) 2008-2013 OpenShot Studios, LLC, Fabrice Bellard + * (http://www.openshotstudios.com). This file is part of + * OpenShot Library (http://www.openshot.org), an open-source project + * dedicated to delivering high quality video editing and animation solutions + * to the world. + * + * This file is originally based on the Libavformat API example, and then modified + * by the libopenshot project. + * + * OpenShot Library is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * * OpenShot Library (libopenshot) is free software: you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * OpenShot Library (libopenshot) is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with OpenShot Library. If not, see . + */ + + +#ifndef OPENSHOT_IMAGE_WRITER_H +#define OPENSHOT_IMAGE_WRITER_H + +#include "ReaderBase.h" +#include "WriterBase.h" + +#include +#include +#include +#include +#include +#include "Magick++.h" +#include "Cache.h" +#include "Exceptions.h" +#include "OpenMPUtilities.h" +#include "Sleep.h" + + +using namespace std; + +namespace openshot +{ + + /** + * @brief This class uses the ImageMagick library to write image files (including animated GIFs) + * + * All image formats supported by ImageMagick are supported by this class. + * + * @code + * // Create a reader for a video + * FFmpegReader r("MyAwesomeVideo.webm"); + * r.Open(); // Open the reader + * + * // Create a writer (which will create an animated GIF file) + * ImageWriter w("/home/jonathan/NewAnimation.gif"); + * + * // Set the image output settings (format, fps, width, height, quality, loops, combine) + * w.SetVideoOptions("GIF", r.info.fps, r.info.width, r.info.height, 70, 1, true); + * + * // Open the writer + * w.Open(); + * + * // Write the 1st 30 frames from the reader + * w.WriteFrame(&r, 1, 30); + * + * // Close the reader & writer + * w.Close(); + * r.Close(); + * @endcode + */ + class ImageWriter : public WriterBase + { + private: + string path; + int cache_size; + bool is_writing; + bool is_open; + int64 write_video_count; + vector frames; + int image_quality; + int number_of_loops; + bool combine_frames; + + tr1::shared_ptr last_frame; + deque > spooled_video_frames; + deque > queued_video_frames; + deque > processed_frames; + deque > deallocate_frames; + + /// write all queued frames + void write_queued_frames(); + + public: + + /// @brief Constructor for ImageWriter. Throws one of the following exceptions. + /// @param path The path of the file you want to create + ImageWriter(string path) throw(InvalidFile, InvalidFormat, InvalidCodec, InvalidOptions, OutOfMemory); + + /// @brief Close the writer and encode/output final image to the disk. This is a requirement of ImageMagick, + /// which writes all frames of a multi-frame image at one time. + void Close(); + + /// @brief Get the cache size + int GetCacheSize() { return cache_size; }; + + /// Determine if writer is open or closed + bool IsOpen() { return is_open; }; + + /// Open writer + void Open() throw(InvalidFile, InvalidCodec); + + /// @brief Set the cache size (number of frames to queue before writing) + /// @param new_size Number of frames to queue before writing + void SetCacheSize(int new_size) { cache_size = new_size; }; + + /// @brief Set the video export options + /// @param format The image format (such as GIF) + /// @param fps Frames per second of the image (used on certain multi-frame image formats, such as GIF) + /// @param width Width in pixels of image + /// @param height Height in pixels of image + /// @param quality Quality of image (0 to 100, 70 is default) + /// @param loops Number of times to repeat the image (used on certain multi-frame image formats, such as GIF) + /// @param combine Combine frames into a single image (if possible), or save each frame as it's own image + void SetVideoOptions(string format, Fraction fps, int width, int height, + int quality, int loops, bool combine); + + /// @brief Add a frame to the stack waiting to be encoded. + /// @param frame The openshot::Frame object to write to this image + void WriteFrame(tr1::shared_ptr frame) throw(WriterClosed); + + /// @brief Write a block of frames from a reader + /// @param reader A openshot::ReaderBase object which will provide frames to be written + /// @param start The starting frame number of the reader + /// @param length The number of frames to write + void WriteFrame(ReaderBase* reader, int start, int length) throw(WriterClosed); + + }; + +} + +#endif diff --git a/include/WriterBase.h b/include/WriterBase.h index 35a487c5..8701c67c 100644 --- a/include/WriterBase.h +++ b/include/WriterBase.h @@ -30,6 +30,7 @@ #include #include +#include "ChannelLayouts.h" #include "Fraction.h" #include "Frame.h" #include "ReaderBase.h" @@ -67,6 +68,7 @@ namespace openshot int audio_bit_rate; ///< The bit rate of the audio stream (in bytes) int sample_rate; ///< The number of audio samples per second (44100 is a common sample rate) int channels; ///< The number of audio channels used in the audio stream + ChannelLayout channel_layout; ///< The channel layout (mono, stereo, 5 point surround, etc...) int audio_stream_index; ///< The index of the audio stream Fraction audio_timebase; ///< The audio timebase determines how long each audio packet should be played }; @@ -97,7 +99,7 @@ namespace openshot string arg6_name, int arg6_value); public: - /// Constructor for WriterBase class, many things are initilized here + /// Constructor for WriterBase class, many things are initialized here WriterBase(); /// Enable or disable debug output. Output will display on the standard output, and you can @@ -107,14 +109,18 @@ namespace openshot /// Information about the current media file WriterInfo info; - /// This method copy's the info struct of a reader, and sets the writer with the same info + /// @brief This method copy's the info struct of a reader, and sets the writer with the same info + /// @param reader The source reader to copy void CopyReaderInfo(ReaderBase* reader); + /// Determine if writer is open or closed + virtual bool IsOpen() = 0; + /// This method is required for all derived classes of WriterBase. Write a Frame to the video file. - virtual void WriteFrame(tr1::shared_ptr frame) = 0; + virtual void WriteFrame(tr1::shared_ptr frame) throw(WriterClosed) = 0; /// This method is required for all derived classes of WriterBase. Write a block of frames from a reader. - virtual void WriteFrame(ReaderBase* reader, int start, int length) = 0; + virtual void WriteFrame(ReaderBase* reader, int start, int length) throw(WriterClosed) = 0; /// Get and Set JSON methods string Json(); ///< Generate JSON string of this object @@ -125,6 +131,9 @@ namespace openshot /// Display file information in the standard output stream (stdout) void DisplayInfo(); + /// Open the writer (and start initializing streams) + virtual void Open() = 0; + /// Output debug information as JSON string OutputDebugJSON(); }; diff --git a/src/ImageWriter.cpp b/src/ImageWriter.cpp new file mode 100644 index 00000000..7c2414bc --- /dev/null +++ b/src/ImageWriter.cpp @@ -0,0 +1,257 @@ +/** + * @file + * @brief Source file for ImageWriter class + * @author Jonathan Thomas , Fabrice Bellard + * + * @section LICENSE + * + * Copyright (c) 2008-2013 OpenShot Studios, LLC, Fabrice Bellard + * (http://www.openshotstudios.com). This file is part of + * OpenShot Library (http://www.openshot.org), an open-source project + * dedicated to delivering high quality video editing and animation solutions + * to the world. + * + * This file is originally based on the Libavformat API example, and then modified + * by the libopenshot project. + * + * OpenShot Library (libopenshot) is free software: you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * OpenShot Library (libopenshot) is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with OpenShot Library. If not, see . + */ + +#include "../include/ImageWriter.h" + +using namespace openshot; + +ImageWriter::ImageWriter(string path) throw (InvalidFile, InvalidFormat, InvalidCodec, InvalidOptions, OutOfMemory) : + path(path), cache_size(8), is_writing(false), write_video_count(0), image_quality(75), number_of_loops(1), + combine_frames(true), is_open(false) +{ + // Disable audio & video (so they can be independently enabled) + info.has_audio = false; + info.has_video = true; +} + +// Set video export options +void ImageWriter::SetVideoOptions(string format, Fraction fps, int width, int height, + int quality, int loops, bool combine) +{ + // Set frames per second (if provided) + info.fps.num = fps.num; + info.fps.den = fps.den; + + // Set image magic properties + image_quality = quality; + number_of_loops = loops; + combine_frames = combine; + info.vcodec = format; + + // Set the timebase (inverse of fps) + info.video_timebase.num = info.fps.den; + info.video_timebase.den = info.fps.num; + + if (width >= 1) + info.width = width; + if (height >= 1) + info.height = height; + + info.video_bit_rate = quality; + + // Calculate the DAR (display aspect ratio) + Fraction size(info.width * info.pixel_ratio.num, info.height * info.pixel_ratio.den); + + // Reduce size fraction + size.Reduce(); + + // Set the ratio based on the reduced fraction + info.display_ratio.num = size.num; + info.display_ratio.den = size.den; + + #pragma omp critical (debug_output) + AppendDebugMethod("ImageWriter::SetVideoOptions (" + format + ")", "width", width, "height", height, "size.num", size.num, "size.den", size.den, "fps.num", fps.num, "fps.den", fps.den); +} + +// Open the writer +void ImageWriter::Open() throw(InvalidFile, InvalidCodec) +{ + is_open = true; +} + +// Add a frame to the queue waiting to be encoded. +void ImageWriter::WriteFrame(tr1::shared_ptr frame) throw(WriterClosed) +{ + // Check for open reader (or throw exception) + if (!is_open) + throw WriterClosed("The FFmpegWriter is closed. Call Open() before calling this method.", path); + + // Add frame pointer to "queue", waiting to be processed the next + // time the WriteFrames() method is called. + spooled_video_frames.push_back(frame); + + #pragma omp critical (debug_output) + AppendDebugMethod("ImageWriter::WriteFrame", "frame->number", frame->number, "spooled_video_frames.size()", spooled_video_frames.size(), "cache_size", cache_size, "is_writing", is_writing, "", -1, "", -1); + + // Write the frames once it reaches the correct cache size + if (spooled_video_frames.size() == cache_size) + { + // Is writer currently writing? + if (!is_writing) + // Write frames to video file + write_queued_frames(); + + else + { + // YES, WRITING... so wait until it finishes, before writing again + while (is_writing) + Sleep(1); // sleep for 250 milliseconds + + // Write frames to video file + write_queued_frames(); + } + } + + // Keep track of the last frame added + last_frame = frame; +} + +// Write all frames in the queue to the video file. +void ImageWriter::write_queued_frames() +{ + #pragma omp critical (debug_output) + AppendDebugMethod("ImageWriter::write_queued_frames", "spooled_video_frames.size()", spooled_video_frames.size(), "", -1, "", -1, "", -1, "", -1, "", -1); + + // Flip writing flag + is_writing = true; + + // Transfer spool to queue + queued_video_frames = spooled_video_frames; + + // Empty spool + spooled_video_frames.clear(); + + // Set the number of threads in OpenMP + omp_set_num_threads(OPEN_MP_NUM_PROCESSORS); + // Allow nested OpenMP sections + omp_set_nested(true); + + #pragma omp parallel + { + #pragma omp single + { + // Loop through each queued image frame + while (!queued_video_frames.empty()) + { + // Get front frame (from the queue) + tr1::shared_ptr frame = queued_video_frames.front(); + + // Add to processed queue + processed_frames.push_back(frame); + + // Copy and resize image + tr1::shared_ptr frame_image = frame->GetImage(); + frame_image->magick( info.vcodec ); + frame_image->backgroundColor(Magick::Color("none")); + frame_image->matte(true); + frame_image->quality(image_quality); + frame_image->animationDelay(info.video_timebase.ToFloat() * 100); + frame_image->animationIterations(number_of_loops); + + // Calculate correct DAR (display aspect ratio) + int new_width = info.width; + int new_height = info.height * frame->GetPixelRatio().Reciprocal().ToDouble(); + + // Resize image + Magick::Geometry new_size(new_width, new_height); + new_size.aspect(true); + frame_image->resize(new_size); + + // Put resized frame in vector (waiting to be written) + frames.push_back(*frame_image.get()); + + // Remove front item + queued_video_frames.pop_front(); + + } // end while + } // end omp single + + #pragma omp single + { + // Loop back through the frames (in order), and write them to the video file + while (!processed_frames.empty()) + { + // Get front frame (from the queue) + tr1::shared_ptr frame = processed_frames.front(); + + // Add to deallocate queue (so we can remove the AVFrames when we are done) + deallocate_frames.push_back(frame); + + // Write frame to video file + // write_video_packet(frame, frame_final); + + // Remove front item + processed_frames.pop_front(); + } + + // Loop through, and deallocate AVFrames + while (!deallocate_frames.empty()) + { + // Get front frame (from the queue) + tr1::shared_ptr frame = deallocate_frames.front(); + + // Remove front item + deallocate_frames.pop_front(); + } + + // Done writing + is_writing = false; + + } // end omp single + } // end omp parallel + +} + +// Write a block of frames from a reader +void ImageWriter::WriteFrame(ReaderBase* reader, int start, int length) throw(WriterClosed) +{ + #pragma omp critical (debug_output) + AppendDebugMethod("ImageWriter::WriteFrame (from Reader)", "start", start, "length", length, "", -1, "", -1, "", -1, "", -1); + + // Loop through each frame (and encoded it) + for (int number = start; number <= length; number++) + { + // Get the frame + tr1::shared_ptr f = reader->GetFrame(number); + + // Encode frame + WriteFrame(f); + } +} + +// Close the writer and encode/output final image to the disk. +void ImageWriter::Close() +{ + // Write frame's image to file + Magick::writeImages(frames.begin(), frames.end(), path, combine_frames); + + // Clear frames vector + frames.clear(); + + // Reset frame counters + write_video_count = 0; + + // Close writer + is_open = false; + + #pragma omp critical (debug_output) + AppendDebugMethod("ImageWriter::Close", "", -1, "", -1, "", -1, "", -1, "", -1, "", -1); +} + diff --git a/src/WriterBase.cpp b/src/WriterBase.cpp index 0e463a79..911e00fe 100644 --- a/src/WriterBase.cpp +++ b/src/WriterBase.cpp @@ -54,6 +54,7 @@ WriterBase::WriterBase() info.audio_bit_rate = 0; info.sample_rate = 0; info.channels = 0; + info.channel_layout = LAYOUT_MONO; info.audio_stream_index = -1; info.audio_timebase = Fraction(); @@ -154,6 +155,7 @@ void WriterBase::CopyReaderInfo(ReaderBase* reader) info.audio_bit_rate = reader->info.audio_bit_rate; info.sample_rate = reader->info.sample_rate; info.channels = reader->info.channels; + info.channel_layout = reader->info.channel_layout; info.audio_stream_index = reader->info.audio_stream_index; info.audio_timebase.num = reader->info.audio_timebase.num; info.audio_timebase.den = reader->info.audio_timebase.den; @@ -192,6 +194,7 @@ void WriterBase::DisplayInfo() { cout << "--> Audio Bit Rate: " << info.audio_bit_rate/1000 << " kb/s" << endl; cout << "--> Sample Rate: " << info.sample_rate << " Hz" << endl; cout << "--> # of Channels: " << info.channels << endl; + cout << "--> Channel Layout: " << info.channel_layout << endl; cout << "--> Audio Stream Index: " << info.audio_stream_index << endl; cout << "--> Audio Timebase: " << info.audio_timebase.ToDouble() << " (" << info.audio_timebase.num << "/" << info.audio_timebase.den << ")" << endl; cout << "----------------------------" << endl; @@ -242,6 +245,7 @@ Json::Value WriterBase::JsonValue() { root["audio_bit_rate"] = info.audio_bit_rate; root["sample_rate"] = info.sample_rate; root["channels"] = info.channels; + root["channel_layout"] = info.channel_layout; root["audio_stream_index"] = info.audio_stream_index; root["audio_timebase"] = Json::Value(Json::objectValue); root["audio_timebase"]["num"] = info.audio_timebase.num; @@ -337,6 +341,8 @@ void WriterBase::SetJsonValue(Json::Value root) { info.sample_rate = root["sample_rate"].asInt(); if (!root["channels"].isNull()) info.channels = root["channels"].asInt(); + if (!root["channel_layout"].isNull()) + info.channel_layout = (ChannelLayout) root["channel_layout"].asInt(); if (!root["audio_stream_index"].isNull()) info.audio_stream_index = root["audio_stream_index"].asInt(); if (!root["audio_timebase"].isNull() && root["audio_timebase"].isObject()) {