diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5acb669
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+build
+.vscode
diff --git a/README.md b/README.md
index 4ab879f..3f96f29 100644
--- a/README.md
+++ b/README.md
@@ -2,24 +2,31 @@
+
Toucan
======
Toucan is a software renderer for OpenTimelineIO files. Toucan can render an
OpenTimelineIO file with multiple tracks, clips, transitions, and effects
into an image sequence or movie file.
-The project currently consists of:
+The project consists of:
* C++ library for rendering timelines
* Collection of OpenFX image effect plugins
* Command line renderer
* Interactive viewer
* Example .otio files
-Current limitations:
-* Audio is not yet supported
-* Nested timelines are not yet supported
-* Exporting movie files currently relies on the FFmpeg command line program
-(see below: FFmpeg Encoding)
+OpenFX Plugins:
+* Generators: Checkers, Fill, Gradient, Noise
+* Drawing: Box, Line, Text
+* Filters: Blur, Color Map, Invert, Power, Saturate, Unsharp Mask
+* Transforms: Crop, Flip, Flop, Resize, Rotate
+* Transitions: Dissolve, Horizontal Wipe, Vertical Wipe
+* Color space: Color Convert, Premultiply Alpha, Un-Premultiply Alpha
+
+TODO:
+* Audio support
+* Nested timeline support
Toucan relies on the following libraries:
* [OpenTimelineIO](https://github.com/PixarAnimationStudios/OpenTimelineIO)
@@ -31,15 +38,6 @@ Toucan relies on the following libraries:
Supported VFX platforms: 2024, 2023, 2022
-OpenFX Plugins
-==============
-The OpenFX image effect plugins include:
-* Generators: Checkers, Fill, Gradient, Noise
-* Drawing: Box, Line, Text
-* Filters: Blur, Color Map, Invert, Power, Saturate, Unsharp Mask
-* Transforms: Crop, Flip, Flop, Resize, Rotate
-* Transitions: Dissolve, Horizontal Wipe, Vertical Wipe
-* Color spaces: Color Convert, Premultiply Alpha, Un-Premultiply Alpha
Example Renders
===============
@@ -91,11 +89,18 @@ Multiple effects on clips, tracks, and stacks:
![Track Effects](images/MultipleEffects.png)
+
FFmpeg Encoding
===============
-Toucan can send rendered images to the FFmpeg command line program for encoding.
-The images can be sent as either the y4m format or raw video. The images are
-piped directly to FFmpeg without the overhead of disk I/O.
+Toucan can write movies with FFmpeg directly, or send raw images to the FFmpeg
+command line program over a pipe.
+
+Example command line writing a movie directly:
+```
+toucan-render Transition.otio Transition.mov -vcodec MJPEG
+```
+
+Raw images can be sent to FFmpeg as either the y4m format or raw video.
Example command line using the y4m format:
```
@@ -127,6 +132,7 @@ can be found by running `toucan-render` with the `-print_size` option.
* `-i pipe:`: Read from standard input instead of a file.
* `output.mov`: The output movie file.
+
Building
========
diff --git a/bin/CMakeLists.txt b/bin/CMakeLists.txt
index 588f766..6df3cac 100644
--- a/bin/CMakeLists.txt
+++ b/bin/CMakeLists.txt
@@ -1,4 +1,5 @@
add_subdirectory(toucan-render)
+add_subdirectory(toucan-filmstrip)
if(toucan_VIEW)
add_subdirectory(toucan-view)
endif()
diff --git a/bin/toucan-filmstrip/App.cpp b/bin/toucan-filmstrip/App.cpp
new file mode 100644
index 0000000..dbe1419
--- /dev/null
+++ b/bin/toucan-filmstrip/App.cpp
@@ -0,0 +1,195 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Contributors to the toucan project.
+
+#include "App.h"
+
+#include
+
+#include
+
+namespace toucan
+{
+ App::App(std::vector& argv)
+ {
+ _exe = argv.front();
+ argv.erase(argv.begin());
+
+ _args.list.push_back(std::make_shared >(
+ _args.input,
+ "input",
+ "Input .otio file."));
+ auto outArg = std::make_shared >(
+ _args.output,
+ "output",
+ "Output image file.");
+ _args.list.push_back(outArg);
+
+ _options.list.push_back(std::make_shared(
+ _options.verbose,
+ std::vector{ "-v" },
+ "Print verbose output."));
+ _options.list.push_back(std::make_shared(
+ _options.help,
+ std::vector{ "-h" },
+ "Print help."));
+
+ if (!argv.empty())
+ {
+ for (const auto& option : _options.list)
+ {
+ option->parse(argv);
+ }
+ if (!_options.help)
+ {
+ for (const auto& arg : _args.list)
+ {
+ arg->parse(argv);
+ }
+ if (argv.size())
+ {
+ _options.help = true;
+ }
+ }
+ }
+ else
+ {
+ _options.help = true;
+ }
+ }
+
+ App::~App()
+ {}
+
+ int App::run()
+ {
+ if (_options.help)
+ {
+ _printHelp();
+ return 1;
+ }
+
+ const std::filesystem::path parentPath = std::filesystem::path(_exe).parent_path();
+ const std::filesystem::path inputPath(_args.input);
+ const std::filesystem::path outputPath(_args.output);
+ const auto outputSplit = splitFileNameNumber(outputPath.stem().string());
+ const int outputStartFrame = atoi(outputSplit.second.c_str());
+ const size_t outputNumberPadding = getNumberPadding(outputSplit.second);
+
+ // Open the timeline.
+ _timelineWrapper = std::make_shared(inputPath);
+
+ // Get time values.
+ const OTIO_NS::TimeRange& timeRange = _timelineWrapper->getTimeRange();
+ const OTIO_NS::RationalTime timeInc(1.0, timeRange.duration().rate());
+ const int frames = timeRange.duration().value();
+
+ // Create the image graph.
+ std::shared_ptr log;
+ if (_options.verbose)
+ {
+ log = std::make_shared();
+ }
+ ImageGraphOptions imageGraphOptions;
+ imageGraphOptions.log = log;
+ _graph = std::make_shared(
+ inputPath.parent_path(),
+ _timelineWrapper,
+ imageGraphOptions);
+ const IMATH_NAMESPACE::V2d imageSize = _graph->getImageSize();
+
+ // Create the image host.
+ std::vector searchPath;
+ searchPath.push_back(parentPath);
+#if defined(_WINDOWS)
+ searchPath.push_back(parentPath / ".." / ".." / "..");
+#else // _WINDOWS
+ searchPath.push_back(parentPath / ".." / "..");
+#endif // _WINDOWS
+ ImageEffectHostOptions imageHostOptions;
+ imageHostOptions.log = log;
+ _host = std::make_shared(
+ searchPath,
+ imageHostOptions);
+
+ // Initialize the filmstrip.
+ OIIO::ImageBuf filmstripBuf;
+ const int thumbnailWidth = 360;
+ const int thumbnailSpacing = 0;
+ IMATH_NAMESPACE::V2d thumbnailSize;
+ if (imageSize.x > 0 && imageSize.y > 0)
+ {
+ thumbnailSize = IMATH_NAMESPACE::V2d(
+ thumbnailWidth,
+ thumbnailWidth / static_cast(imageSize.x / static_cast(imageSize.y)));
+ const IMATH_NAMESPACE::V2d filmstripSize(
+ thumbnailSize.x * frames + thumbnailSpacing * (frames - 1),
+ thumbnailSize.y);
+ filmstripBuf = OIIO::ImageBufAlgo::fill(
+ { 0.F, 0.F, 0.F, 0.F },
+ OIIO::ROI(0, filmstripSize.x, 0, filmstripSize.y, 0, 1, 0, 4));
+ }
+
+ // Render the timeline frames.
+ int filmstripX = 0;
+ for (OTIO_NS::RationalTime time = timeRange.start_time();
+ time <= timeRange.end_time_inclusive();
+ time += timeInc)
+ {
+ std::cout << (time - timeRange.start_time()).value() << "/" <<
+ timeRange.duration().value() << std::endl;
+
+ if (auto node = _graph->exec(_host, time))
+ {
+ // Execute the graph.
+ const auto buf = node->exec();
+
+ // Append the image.
+ const auto thumbnailBuf = OIIO::ImageBufAlgo::resize(
+ buf,
+ "",
+ 0.0,
+ OIIO::ROI(0, thumbnailSize.x, 0, thumbnailSize.y, 0, 1, 0, 4));
+ OIIO::ImageBufAlgo::paste(
+ filmstripBuf,
+ filmstripX,
+ 0,
+ 0,
+ 0,
+ thumbnailBuf);
+ filmstripX += thumbnailSize.x + thumbnailSpacing;
+ }
+ }
+
+ // Write the image.
+ filmstripBuf.write(outputPath.string());
+
+ return 0;
+ }
+
+ void App::_printHelp()
+ {
+ std::cout << "Usage:" << std::endl;
+ std::cout << std::endl;
+ std::cout << " toucan-filmstrip (input) (output) [options...]" << std::endl;
+ std::cout << std::endl;
+ std::cout << "Arguments:" << std::endl;
+ std::cout << std::endl;
+ for (const auto& arg : _args.list)
+ {
+ std::cout << " " << arg->getName() << " - " << arg->getHelp() << std::endl;
+ std::cout << std::endl;
+ }
+ std::cout << std::endl;
+ std::cout << "Options:" << std::endl;
+ std::cout << std::endl;
+ for (const auto& option : _options.list)
+ {
+ for (const auto& line : option->getHelp())
+ {
+ std::cout << " " << line << std::endl;
+ }
+ std::cout << std::endl;
+ }
+ }
+}
+
diff --git a/bin/toucan-filmstrip/App.h b/bin/toucan-filmstrip/App.h
new file mode 100644
index 0000000..3191cb2
--- /dev/null
+++ b/bin/toucan-filmstrip/App.h
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Contributors to the toucan project.
+
+#pragma once
+
+
+#include
+#include
+#include
+#include
+
+#include
+
+namespace toucan
+{
+ class App : public std::enable_shared_from_this
+ {
+ public:
+ App(std::vector&);
+
+ ~App();
+
+ int run();
+
+ private:
+ void _printHelp();
+
+ std::string _exe;
+
+ struct Args
+ {
+ std::string input;
+ std::string output;
+ std::vector > list;
+ };
+ Args _args;
+
+ struct Options
+ {
+ bool verbose = false;
+ bool help = false;
+ std::vector > list;
+ };
+ Options _options;
+
+ std::shared_ptr _timelineWrapper;
+ std::shared_ptr _graph;
+ std::shared_ptr _host;
+ };
+}
+
diff --git a/bin/toucan-filmstrip/CMakeLists.txt b/bin/toucan-filmstrip/CMakeLists.txt
new file mode 100644
index 0000000..2222bcc
--- /dev/null
+++ b/bin/toucan-filmstrip/CMakeLists.txt
@@ -0,0 +1,21 @@
+set(HEADERS
+ App.h)
+set(SOURCE
+ App.cpp
+ main.cpp)
+
+add_executable(toucan-filmstrip ${HEADERS} ${SOURCE})
+target_link_libraries(toucan-filmstrip toucan)
+set_target_properties(toucan-filmstrip PROPERTIES FOLDER bin)
+add_dependencies(toucan-filmstrip ${TOUCAN_PLUGINS})
+
+install(
+ TARGETS toucan-filmstrip
+ RUNTIME DESTINATION bin)
+
+foreach(OTIO CompositeTracks Draw Filter Gap Generator LinearTimeWarp Transition Transition2 Transform)
+ add_test(
+ toucan-filmstrip-${OTIO}
+ ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/toucan-filmstrip${CMAKE_EXECUTABLE_SUFFIX}
+ ${PROJECT_SOURCE_DIR}/data/${OTIO}.otio ${OTIO}.png)
+endforeach()
diff --git a/bin/toucan-filmstrip/main.cpp b/bin/toucan-filmstrip/main.cpp
new file mode 100644
index 0000000..44739a1
--- /dev/null
+++ b/bin/toucan-filmstrip/main.cpp
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Contributors to the toucan project.
+
+#include "App.h"
+
+#include
+
+using namespace toucan;
+
+int main(int argc, char** argv)
+{
+ int out = 1;
+ std::vector args;
+ for (int i = 0; i < argc; ++i)
+ {
+ args.push_back(argv[i]);
+ }
+ try
+ {
+ auto app = std::make_shared(args);
+ out = app->run();
+ }
+ catch (const std::exception& e)
+ {
+ std::cout << "ERROR: " << e.what() << std::endl;
+ }
+ return out;
+}
diff --git a/bin/toucan-render/App.cpp b/bin/toucan-render/App.cpp
index b797e80..8802969 100644
--- a/bin/toucan-render/App.cpp
+++ b/bin/toucan-render/App.cpp
@@ -3,8 +3,7 @@
#include "App.h"
-#include "Util.h"
-
+#include
#include
#include
@@ -53,7 +52,7 @@ namespace toucan
auto outArg = std::make_shared >(
_args.output,
"output",
- "Output image file. Use a dash ('-') to write raw frames or y4m to stdout.");
+ "Output image or movie file. Use a dash ('-') to write raw frames or y4m to stdout.");
_args.list.push_back(outArg);
std::vector rawList;
@@ -66,6 +65,12 @@ namespace toucan
{
y4mList.push_back(spec.first);
}
+ _options.list.push_back(std::make_shared >(
+ _options.videoCodec,
+ std::vector{ "-vcodec" },
+ "Set the video codec.",
+ _options.videoCodec,
+ join(ffmpeg::getVideoCodecStrings(), ", ")));
_options.list.push_back(std::make_shared(
_options.printStart,
std::vector{ "-print_start" },
@@ -94,14 +99,6 @@ namespace toucan
"y4m format to send to stdout.",
_options.y4m,
join(y4mList, ", ")));
- _options.list.push_back(std::make_shared(
- _options.filmstrip,
- std::vector{ "-filmstrip" },
- "Render the frames to a single output image as thumbnails in a row."));
- _options.list.push_back(std::make_shared(
- _options.graph,
- std::vector{ "-graph" },
- "Write a Graphviz graph for each frame."));
_options.list.push_back(std::make_shared(
_options.verbose,
std::vector{ "-v" },
@@ -240,22 +237,17 @@ namespace toucan
searchPath,
imageHostOptions);
- // Initialize the filmstrip.
- OIIO::ImageBuf filmstripBuf;
- const int thumbnailWidth = 360;
- const int thumbnailSpacing = 0;
- IMATH_NAMESPACE::V2d thumbnailSize;
- if (_options.filmstrip && imageSize.x > 0 && imageSize.y > 0)
- {
- thumbnailSize = IMATH_NAMESPACE::V2d(
- thumbnailWidth,
- thumbnailWidth / static_cast(imageSize.x / static_cast(imageSize.y)));
- const IMATH_NAMESPACE::V2d filmstripSize(
- thumbnailSize.x * frames + thumbnailSpacing * (frames - 1),
- thumbnailSize.y);
- filmstripBuf = OIIO::ImageBufAlgo::fill(
- { 0.F, 0.F, 0.F, 0.F },
- OIIO::ROI(0, filmstripSize.x, 0, filmstripSize.y, 0, 1, 0, 4));
+ // Open the movie file.
+ std::shared_ptr ffWrite;
+ if (ffmpeg::hasVideoExtension(outputPath.extension().string()))
+ {
+ ffmpeg::VideoCodec videoCodec = ffmpeg::VideoCodec::First;
+ ffmpeg::fromString(_options.videoCodec, videoCodec);
+ ffWrite = std::make_shared(
+ outputPath,
+ OIIO::ImageSpec(imageSize.x, imageSize.y, 3),
+ timeRange,
+ videoCodec);
}
// Render the timeline frames.
@@ -263,7 +255,6 @@ namespace toucan
{
_writeY4mHeader();
}
- int filmstripX = 0;
for (OTIO_NS::RationalTime time = timeRange.start_time();
time <= timeRange.end_time_inclusive();
time += timeInc)
@@ -280,9 +271,13 @@ namespace toucan
const auto buf = node->exec();
// Save the image.
- if (!_options.filmstrip)
+ if (!_args.outputRaw)
{
- if (!_args.outputRaw)
+ if (ffWrite)
+ {
+ ffWrite->writeImage(buf, time);
+ }
+ else
{
const std::string fileName = getSequenceFrame(
outputPath.parent_path().string(),
@@ -292,68 +287,17 @@ namespace toucan
outputPath.extension().string());
buf.write(fileName);
}
- else if (!_options.raw.empty())
- {
- _writeRawFrame(buf);
- }
- else if (!_options.y4m.empty())
- {
- _writeY4mFrame(buf);
- }
}
- else
+ else if (!_options.raw.empty())
{
- const auto thumbnailBuf = OIIO::ImageBufAlgo::resize(
- buf,
- "",
- 0.0,
- OIIO::ROI(0, thumbnailSize.x, 0, thumbnailSize.y, 0, 1, 0, 4));
- OIIO::ImageBufAlgo::paste(
- filmstripBuf,
- filmstripX,
- 0,
- 0,
- 0,
- thumbnailBuf);
- filmstripX += thumbnailSize.x + thumbnailSpacing;
+ _writeRawFrame(buf);
}
-
- // Write the graph.
- if (_options.graph)
+ else if (!_options.y4m.empty())
{
- const std::string fileName = getSequenceFrame(
- outputPath.parent_path().string(),
- outputSplit.first,
- outputStartFrame + time.to_frames(),
- outputNumberPadding,
- ".dot");
- const std::vector lines = node->graph(inputPath.stem().string());
- if (FILE* f = fopen(fileName.c_str(), "w"))
- {
- for (const auto& line : lines)
- {
- fprintf(f, "%s\n", line.c_str());
- }
- fclose(f);
- }
+ _writeY4mFrame(buf);
}
}
}
- if (_options.filmstrip)
- {
- if (!_args.outputRaw)
- {
- filmstripBuf.write(outputPath.string());
- }
- else if (!_options.raw.empty())
- {
- _writeRawFrame(filmstripBuf);
- }
- else if (!_options.y4m.empty())
- {
- _writeY4mFrame(filmstripBuf);
- }
- }
return 0;
}
diff --git a/bin/toucan-render/App.h b/bin/toucan-render/App.h
index 4714601..3036ce1 100644
--- a/bin/toucan-render/App.h
+++ b/bin/toucan-render/App.h
@@ -3,8 +3,7 @@
#pragma once
-#include "CmdLine.h"
-
+#include
#include
#include
#include
@@ -48,14 +47,13 @@ namespace toucan
struct Options
{
+ std::string videoCodec = "MJPEG";
bool printStart = false;
bool printDuration = false;
bool printRate = false;
bool printSize = false;
std::string raw;
std::string y4m;
- bool filmstrip = false;
- bool graph = false;
bool verbose = false;
bool help = false;
std::vector > list;
diff --git a/bin/toucan-render/CMakeLists.txt b/bin/toucan-render/CMakeLists.txt
index 1217699..c6837d4 100644
--- a/bin/toucan-render/CMakeLists.txt
+++ b/bin/toucan-render/CMakeLists.txt
@@ -1,12 +1,7 @@
set(HEADERS
- App.h
- CmdLine.h
- CmdLineInline.h
- Util.h)
+ App.h)
set(SOURCE
App.cpp
- CmdLine.cpp
- Util.cpp
main.cpp)
add_executable(toucan-render ${HEADERS} ${SOURCE})
@@ -22,6 +17,6 @@ foreach(OTIO CompositeTracks Draw Filter Gap Generator LinearTimeWarp Transition
add_test(
toucan-render-${OTIO}
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/toucan-render${CMAKE_EXECUTABLE_SUFFIX}
- ${PROJECT_SOURCE_DIR}/data/${OTIO}.otio ${OTIO}.png -filmstrip)
+ ${PROJECT_SOURCE_DIR}/data/${OTIO}.otio ${OTIO}.png)
endforeach()
diff --git a/bin/toucan-render/Util.cpp b/bin/toucan-render/Util.cpp
deleted file mode 100644
index f680b73..0000000
--- a/bin/toucan-render/Util.cpp
+++ /dev/null
@@ -1,38 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// Copyright Contributors to the toucan project.
-
-#include "Util.h"
-
-namespace toucan
-{
- std::string join(const std::vector& values, char delimeter)
- {
- std::string out;
- const std::size_t size = values.size();
- for (std::size_t i = 0; i < size; ++i)
- {
- out += values[i];
- if (i < size - 1)
- {
- out += delimeter;
- }
- }
- return out;
- }
-
- std::string join(const std::vector& values, const std::string& delimeter)
- {
- std::string out;
- const std::size_t size = values.size();
- for (std::size_t i = 0; i < size; ++i)
- {
- out += values[i];
- if (i < size - 1)
- {
- out += delimeter;
- }
- }
- return out;
- }
-}
-
diff --git a/bin/toucan-render/Util.h b/bin/toucan-render/Util.h
deleted file mode 100644
index 9b92448..0000000
--- a/bin/toucan-render/Util.h
+++ /dev/null
@@ -1,17 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// Copyright Contributors to the toucan project.
-
-#pragma once
-
-#include
-#include
-
-namespace toucan
-{
- //! Join a list of strings.
- std::string join(const std::vector&, char delimeter);
-
- //! Join a list of strings.
- std::string join(const std::vector&, const std::string& delimeter);
-}
-
diff --git a/cmake/Package.cmake b/cmake/Package.cmake
index a2a21db..4577e12 100644
--- a/cmake/Package.cmake
+++ b/cmake/Package.cmake
@@ -25,22 +25,22 @@ elseif(APPLE)
#set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
set(INSTALL_DYLIBS
- ${CMAKE_INSTALL_PREFIX}/lib/libavcodec.61.3.100.dylib
+ ${CMAKE_INSTALL_PREFIX}/lib/libavcodec.61.19.100.dylib
${CMAKE_INSTALL_PREFIX}/lib/libavcodec.61.dylib
${CMAKE_INSTALL_PREFIX}/lib/libavcodec.dylib
- ${CMAKE_INSTALL_PREFIX}/lib/libavdevice.61.1.100.dylib
+ ${CMAKE_INSTALL_PREFIX}/lib/libavdevice.61.3.100.dylib
${CMAKE_INSTALL_PREFIX}/lib/libavdevice.61.dylib
${CMAKE_INSTALL_PREFIX}/lib/libavdevice.dylib
- ${CMAKE_INSTALL_PREFIX}/lib/libavformat.61.1.100.dylib
+ ${CMAKE_INSTALL_PREFIX}/lib/libavformat.61.7.100.dylib
${CMAKE_INSTALL_PREFIX}/lib/libavformat.61.dylib
${CMAKE_INSTALL_PREFIX}/lib/libavformat.dylib
- ${CMAKE_INSTALL_PREFIX}/lib/libavutil.59.8.100.dylib
+ ${CMAKE_INSTALL_PREFIX}/lib/libavutil.59.39.100.dylib
${CMAKE_INSTALL_PREFIX}/lib/libavutil.59.dylib
${CMAKE_INSTALL_PREFIX}/lib/libavutil.dylib
- ${CMAKE_INSTALL_PREFIX}/lib/libswresample.5.1.100.dylib
+ ${CMAKE_INSTALL_PREFIX}/lib/libswresample.5.3.100.dylib
${CMAKE_INSTALL_PREFIX}/lib/libswresample.5.dylib
${CMAKE_INSTALL_PREFIX}/lib/libswresample.dylib
- ${CMAKE_INSTALL_PREFIX}/lib/libswscale.8.1.100.dylib
+ ${CMAKE_INSTALL_PREFIX}/lib/libswscale.8.3.100.dylib
${CMAKE_INSTALL_PREFIX}/lib/libswscale.8.dylib
${CMAKE_INSTALL_PREFIX}/lib/libswscale.dylib)
@@ -55,22 +55,22 @@ else()
set(INSTALL_LIBS
${CMAKE_INSTALL_PREFIX}/lib/libavcodec.so
${CMAKE_INSTALL_PREFIX}/lib/libavcodec.so.61
- ${CMAKE_INSTALL_PREFIX}/lib/libavcodec.so.61.3.100
+ ${CMAKE_INSTALL_PREFIX}/lib/libavcodec.so.61.19.100
${CMAKE_INSTALL_PREFIX}/lib/libavdevice.so
${CMAKE_INSTALL_PREFIX}/lib/libavdevice.so.61
- ${CMAKE_INSTALL_PREFIX}/lib/libavdevice.so.61.1.100
+ ${CMAKE_INSTALL_PREFIX}/lib/libavdevice.so.61.3.100
${CMAKE_INSTALL_PREFIX}/lib/libavformat.so
${CMAKE_INSTALL_PREFIX}/lib/libavformat.so.61
- ${CMAKE_INSTALL_PREFIX}/lib/libavformat.so.61.1.100
+ ${CMAKE_INSTALL_PREFIX}/lib/libavformat.so.61.7.100
${CMAKE_INSTALL_PREFIX}/lib/libavutil.so
${CMAKE_INSTALL_PREFIX}/lib/libavutil.so.59
- ${CMAKE_INSTALL_PREFIX}/lib/libavutil.so.59.8.100
+ ${CMAKE_INSTALL_PREFIX}/lib/libavutil.so.59.39.100
${CMAKE_INSTALL_PREFIX}/lib/libswresample.so
${CMAKE_INSTALL_PREFIX}/lib/libswresample.so.5
- ${CMAKE_INSTALL_PREFIX}/lib/libswresample.so.5.1.100
+ ${CMAKE_INSTALL_PREFIX}/lib/libswresample.so.5.3.100
${CMAKE_INSTALL_PREFIX}/lib/libswscale.so
${CMAKE_INSTALL_PREFIX}/lib/libswscale.so.8
- ${CMAKE_INSTALL_PREFIX}/lib/libswscale.so.8.1.100)
+ ${CMAKE_INSTALL_PREFIX}/lib/libswscale.so.8.3.100)
install(
FILES ${INSTALL_LIBS}
diff --git a/cmake/SuperBuild/BuildFFmpeg.cmake b/cmake/SuperBuild/BuildFFmpeg.cmake
index 4f8b362..072afee 100644
--- a/cmake/SuperBuild/BuildFFmpeg.cmake
+++ b/cmake/SuperBuild/BuildFFmpeg.cmake
@@ -6,6 +6,9 @@ if(WIN32)
endif()
set(FFmpeg_DEPS ZLIB)
+if(toucan_svt-av1)
+ list(APPEND FFmpeg_DEPS svt-av1)
+endif()
if(toucan_NET)
list(APPEND FFmpeg_DEPS OpenSSL)
endif()
@@ -42,12 +45,14 @@ if(APPLE AND CMAKE_OSX_DEPLOYMENT_TARGET)
list(APPEND FFmpeg_OBJCFLAGS "--extra-objcflags=-mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}")
list(APPEND FFmpeg_LDFLAGS "--extra-ldflags=-mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}")
endif()
+
if(FFmpeg_DEBUG)
list(APPEND FFmpeg_CFLAGS "--extra-cflags=-g")
list(APPEND FFmpeg_CXXFLAGS "--extra-cxxflags=-g")
list(APPEND FFmpeg_OBJCFLAGS "--extra-objcflags=-g")
list(APPEND FFmpeg_LDFLAGS "--extra-ldflags=-g")
endif()
+
set(FFmpeg_CONFIGURE_ARGS
--prefix=${CMAKE_INSTALL_PREFIX}
--disable-doc
@@ -156,6 +161,7 @@ if(toucan_FFmpeg_MINIMAL)
--enable-encoder=ac3
--enable-encoder=dnxhd
--enable-encoder=eac3
+ --enable-encoder=libsvtav1
--enable-encoder=mjpeg
--enable-encoder=mpeg2video
--enable-encoder=mpeg4
@@ -303,12 +309,18 @@ if(toucan_FFmpeg_MINIMAL)
--enable-protocol=https
--enable-protocol=md5
--enable-protocol=pipe
- --enable-protocol=tls)
+ --enable-protocol=tls
+ --disable-filters)
endif()
if(NOT WIN32)
list(APPEND FFmpeg_CONFIGURE_ARGS
--x86asmexe=${CMAKE_INSTALL_PREFIX}/bin/nasm)
endif()
+if(toucan_svt-av1)
+ list(APPEND FFmpeg_CONFIGURE_ARGS
+ --enable-libsvtav1
+ --pkg-config-flags=--with-path=${CMAKE_INSTALL_PREFIX}/lib/pkgconfig)
+endif()
if(toucan_NET)
list(APPEND FFmpeg_CONFIGURE_ARGS
--enable-openssl)
@@ -325,6 +337,7 @@ if(FFmpeg_DEBUG)
--enable-debug=3
--assert-level=2)
endif()
+
include(ProcessorCount)
ProcessorCount(FFmpeg_BUILD_JOBS)
if(WIN32)
@@ -366,34 +379,34 @@ else()
set(FFmpeg_INSTALL make install)
if(APPLE)
list(APPEND FFmpeg_INSTALL
- COMMAND install_name_tool -id @rpath/libavcodec.61.3.100.dylib ${CMAKE_INSTALL_PREFIX}/lib/libavcodec.61.dylib
- COMMAND install_name_tool -id @rpath/libavdevice.61.1.100.dylib ${CMAKE_INSTALL_PREFIX}/lib/libavdevice.61.dylib
- COMMAND install_name_tool -id @rpath/libavformat.61.1.100.dylib ${CMAKE_INSTALL_PREFIX}/lib/libavformat.61.dylib
- COMMAND install_name_tool -id @rpath/libavutil.59.8.100.dylib ${CMAKE_INSTALL_PREFIX}/lib/libavutil.59.dylib
- COMMAND install_name_tool -id @rpath/libswresample.5.1.100.dylib ${CMAKE_INSTALL_PREFIX}/lib/libswresample.5.dylib
- COMMAND install_name_tool -id @rpath/libswscale.8.1.100.dylib ${CMAKE_INSTALL_PREFIX}/lib/libswscale.8.dylib
+ COMMAND install_name_tool -id @rpath/libavcodec.61.19.100.dylib ${CMAKE_INSTALL_PREFIX}/lib/libavcodec.61.dylib
+ COMMAND install_name_tool -id @rpath/libavdevice.61.3.100.dylib ${CMAKE_INSTALL_PREFIX}/lib/libavdevice.61.dylib
+ COMMAND install_name_tool -id @rpath/libavformat.61.7.100.dylib ${CMAKE_INSTALL_PREFIX}/lib/libavformat.61.dylib
+ COMMAND install_name_tool -id @rpath/libavutil.59.39.100.dylib ${CMAKE_INSTALL_PREFIX}/lib/libavutil.59.dylib
+ COMMAND install_name_tool -id @rpath/libswresample.5.3.100.dylib ${CMAKE_INSTALL_PREFIX}/lib/libswresample.5.dylib
+ COMMAND install_name_tool -id @rpath/libswscale.8.3.100.dylib ${CMAKE_INSTALL_PREFIX}/lib/libswscale.8.dylib
COMMAND install_name_tool
-change ${CMAKE_INSTALL_PREFIX}/lib/libswresample.5.dylib @rpath/libswresample.5.dylib
-change ${CMAKE_INSTALL_PREFIX}/lib/libavutil.59.dylib @rpath/libavutil.59.dylib
- ${CMAKE_INSTALL_PREFIX}/lib/libavcodec.61.3.100.dylib
+ ${CMAKE_INSTALL_PREFIX}/lib/libavcodec.61.19.100.dylib
COMMAND install_name_tool
-change ${CMAKE_INSTALL_PREFIX}/lib/libswscale.8.dylib @rpath/libswscale.8.dylib
-change ${CMAKE_INSTALL_PREFIX}/lib/libavformat.61.dylib @rpath/libavformat.61.dylib
-change ${CMAKE_INSTALL_PREFIX}/lib/libavcodec.61.dylib @rpath/libavcodec.61.dylib
-change ${CMAKE_INSTALL_PREFIX}/lib/libswresample.5.dylib @rpath/libswresample.5.dylib
-change ${CMAKE_INSTALL_PREFIX}/lib/libavutil.59.dylib @rpath/libavutil.59.dylib
- ${CMAKE_INSTALL_PREFIX}/lib/libavdevice.61.1.100.dylib
+ ${CMAKE_INSTALL_PREFIX}/lib/libavdevice.61.3.100.dylib
COMMAND install_name_tool
-change ${CMAKE_INSTALL_PREFIX}/lib/libavcodec.61.dylib @rpath/libavcodec.61.dylib
-change ${CMAKE_INSTALL_PREFIX}/lib/libswresample.5.dylib @rpath/libswresample.5.dylib
-change ${CMAKE_INSTALL_PREFIX}/lib/libavutil.59.dylib @rpath/libavutil.59.dylib
- ${CMAKE_INSTALL_PREFIX}/lib/libavformat.61.1.100.dylib
+ ${CMAKE_INSTALL_PREFIX}/lib/libavformat.61.7.100.dylib
COMMAND install_name_tool
-change ${CMAKE_INSTALL_PREFIX}/lib/libavutil.59.dylib @rpath/libavutil.59.dylib
- ${CMAKE_INSTALL_PREFIX}/lib/libswresample.5.1.100.dylib
+ ${CMAKE_INSTALL_PREFIX}/lib/libswresample.5.3.100.dylib
COMMAND install_name_tool
-change ${CMAKE_INSTALL_PREFIX}/lib/libavutil.59.dylib @rpath/libavutil.59.dylib
- ${CMAKE_INSTALL_PREFIX}/lib/libswscale.8.1.100.dylib)
+ ${CMAKE_INSTALL_PREFIX}/lib/libswscale.8.3.100.dylib)
endif()
endif()
@@ -401,8 +414,8 @@ ExternalProject_Add(
FFmpeg
PREFIX ${CMAKE_CURRENT_BINARY_DIR}/FFmpeg
DEPENDS ${FFmpeg_DEPS}
- URL https://ffmpeg.org/releases/ffmpeg-7.0.1.tar.bz2
- CONFIGURE_COMMAND ${FFmpeg_CONFIGURE}
+ URL https://ffmpeg.org/releases/ffmpeg-7.1.tar.bz2
+ CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env PKG_CONFIG_PATH=${CMAKE_INSTALL_PREFIX}/lib/pkgconfig ${FFmpeg_CONFIGURE}
BUILD_COMMAND ${FFmpeg_BUILD}
INSTALL_COMMAND ${FFmpeg_INSTALL}
BUILD_IN_SOURCE 1)
diff --git a/cmake/SuperBuild/BuildJPEG.cmake b/cmake/SuperBuild/BuildJPEG.cmake
index 6c7a91b..f37d6a0 100644
--- a/cmake/SuperBuild/BuildJPEG.cmake
+++ b/cmake/SuperBuild/BuildJPEG.cmake
@@ -5,7 +5,7 @@ set(libjpeg-turbo_GIT_TAG "3.0.0")
set(libjpeg-turbo_DEPS ZLIB)
if(NOT WIN32)
- set(libjpeg-turbo_DEPS ${libjpeg-turbo_DEPS} NASM)
+ list(APPEND libjpeg-turbo_DEPS NASM)
endif()
set(libjpeg-turbo_ENABLE_SHARED ON)
diff --git a/cmake/SuperBuild/Builddtk-deps.cmake b/cmake/SuperBuild/Builddtk-deps.cmake
index ac0c09e..a0107f0 100644
--- a/cmake/SuperBuild/Builddtk-deps.cmake
+++ b/cmake/SuperBuild/Builddtk-deps.cmake
@@ -1,7 +1,7 @@
include(ExternalProject)
set(dtk_GIT_REPOSITORY "https://github.com/darbyjohnston/dtk.git")
-set(dtk_GIT_TAG "b9515ed232f27dbba1398a81d25dad0d2caf231a")
+set(dtk_GIT_TAG "b5d7a808efae236ee1a4635956cf500fae9528e2")
set(dtk-deps_ARGS
${toucan_EXTERNAL_PROJECT_ARGS}
diff --git a/cmake/SuperBuild/Builddtk.cmake b/cmake/SuperBuild/Builddtk.cmake
index 8e66eaa..8981a6a 100644
--- a/cmake/SuperBuild/Builddtk.cmake
+++ b/cmake/SuperBuild/Builddtk.cmake
@@ -1,7 +1,7 @@
include(ExternalProject)
set(dtk_GIT_REPOSITORY "https://github.com/darbyjohnston/dtk.git")
-set(dtk_GIT_TAG "b9515ed232f27dbba1398a81d25dad0d2caf231a")
+set(dtk_GIT_TAG "b5d7a808efae236ee1a4635956cf500fae9528e2")
set(dtk_DEPS dtk-deps)
set(dtk_ARGS
diff --git a/cmake/SuperBuild/Buildsvt-av1.cmake b/cmake/SuperBuild/Buildsvt-av1.cmake
new file mode 100644
index 0000000..818d36e
--- /dev/null
+++ b/cmake/SuperBuild/Buildsvt-av1.cmake
@@ -0,0 +1,27 @@
+include(ExternalProject)
+
+set(svt-av1_GIT_REPOSITORY "https://gitlab.com/AOMediaCodec/SVT-AV1.git")
+set(svt-av1_GIT_TAG "v2.3.0")
+
+set(svt-av1_DEPS)
+if(NOT WIN32)
+ list(APPEND svt-av1_DEPS NASM)
+endif()
+
+set(svt-av1_PATCH ${CMAKE_COMMAND} -E copy_if_different
+ ${CMAKE_CURRENT_SOURCE_DIR}/svt-av1-patch/Source/Lib/pkg-config.pc.in
+ ${CMAKE_CURRENT_BINARY_DIR}/svt-av1/src/svt-av1/Source/Lib/pkg-config.pc.in)
+
+set(svt-av1_ARGS
+ ${toucan_EXTERNAL_PROJECT_ARGS}
+ -DBUILD_APPS=OFF
+ -DCMAKE_POSITION_INDEPENDENT_CODE=ON)
+
+ExternalProject_Add(
+ svt-av1
+ PREFIX ${CMAKE_CURRENT_BINARY_DIR}/svt-av1
+ DEPENDS ${svt-av1_DEPS}
+ GIT_REPOSITORY ${svt-av1_GIT_REPOSITORY}
+ GIT_TAG ${svt-av1_GIT_TAG}
+ PATCH_COMMAND ${svt-av1_PATCH}
+ CMAKE_ARGS ${svt-av1_ARGS})
diff --git a/cmake/SuperBuild/CMakeLists.txt b/cmake/SuperBuild/CMakeLists.txt
index 03fdfab..2372537 100644
--- a/cmake/SuperBuild/CMakeLists.txt
+++ b/cmake/SuperBuild/CMakeLists.txt
@@ -15,6 +15,11 @@ set(toucan_JPEG ON CACHE BOOL "Build JPEG")
set(toucan_TIFF ON CACHE BOOL "Build TIFF")
set(toucan_Imath ON CACHE BOOL "Build Imath")
set(toucan_OpenEXR ON CACHE BOOL "Build OpenEXR")
+set(toucan_svt-av1_DEFAULT OFF)
+if(NOT WIN32 AND NOT APPLE)
+ set(toucan_svt-av1_DEFAULT ON)
+endif()
+set(toucan_svt-av1 ${toucan_svt-av1_DEFAULT} CACHE BOOL "Build SVT-AV1")
set(toucan_FFmpeg ON CACHE BOOL "Build FFmpeg")
set(toucan_FFmpeg_MINIMAL ON CACHE BOOL "Build a minimal set of FFmpeg codecs")
set(toucan_OpenColorIO ON CACHE BOOL "Build OpenColorIO")
@@ -83,7 +88,10 @@ if(toucan_Imath)
include(BuildImath)
endif()
if(toucan_OpenEXR)
-include(BuildOpenEXR)
+ include(BuildOpenEXR)
+endif()
+if(toucan_svt-av1)
+ include(Buildsvt-av1.cmake)
endif()
if(toucan_FFmpeg)
include(BuildFFmpeg)
diff --git a/cmake/SuperBuild/svt-av1-patch/Source/Lib/pkg-config.pc.in b/cmake/SuperBuild/svt-av1-patch/Source/Lib/pkg-config.pc.in
new file mode 100644
index 0000000..a55ce28
--- /dev/null
+++ b/cmake/SuperBuild/svt-av1-patch/Source/Lib/pkg-config.pc.in
@@ -0,0 +1,12 @@
+prefix=@CMAKE_INSTALL_PREFIX@
+exec_prefix=${prefix}
+includedir=@SVT_AV1_INCLUDEDIR@
+libdir=@SVT_AV1_LIBDIR@
+
+Name: SvtAv1Enc
+Description: SVT (Scalable Video Technology) for AV1 encoder library
+Version: @ENC_VERSION_MAJOR@.@ENC_VERSION_MINOR@.@ENC_VERSION_PATCH@
+Libs: -L${libdir} -lSvtAv1Enc @LIBS_PRIVATE@
+Libs.private: @LIBS_PRIVATE@
+Cflags: -I${includedir}/svt-av1@ENC_PKG_CONFIG_EXTRA_CFLAGS@
+Cflags.private: -UEB_DLL
diff --git a/legal/LICENSE_svt-av1.md b/legal/LICENSE_svt-av1.md
new file mode 100644
index 0000000..aff96d1
--- /dev/null
+++ b/legal/LICENSE_svt-av1.md
@@ -0,0 +1,32 @@
+BSD 3-Clause Clear License
+The Clear BSD License
+
+Copyright (c) 2021, Alliance for Open Media
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted (subject to the limitations in the disclaimer below)
+provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the Alliance for Open Media nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE.
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/toucan/CMakeLists.txt b/lib/toucan/CMakeLists.txt
index 1a20074..301f2c9 100644
--- a/lib/toucan/CMakeLists.txt
+++ b/lib/toucan/CMakeLists.txt
@@ -1,6 +1,10 @@
set(HEADERS
+ CmdLine.h
+ CmdLineInline.h
Comp.h
+ FFmpeg.h
FFmpegRead.h
+ FFmpegWrite.h
ImageEffect.h
ImageEffectHost.h
ImageGraph.h
@@ -19,8 +23,11 @@ set(HEADERS
set(HEADERS_PRIVATE
ImageEffect_p.h)
set(SOURCE
+ CmdLine.cpp
Comp.cpp
+ FFmpeg.cpp
FFmpegRead.cpp
+ FFmpegWrite.cpp
ImageEffect.cpp
ImageEffectHost.cpp
ImageGraph.cpp
diff --git a/bin/toucan-render/CmdLine.cpp b/lib/toucan/CmdLine.cpp
similarity index 100%
rename from bin/toucan-render/CmdLine.cpp
rename to lib/toucan/CmdLine.cpp
diff --git a/bin/toucan-render/CmdLine.h b/lib/toucan/CmdLine.h
similarity index 100%
rename from bin/toucan-render/CmdLine.h
rename to lib/toucan/CmdLine.h
diff --git a/bin/toucan-render/CmdLineInline.h b/lib/toucan/CmdLineInline.h
similarity index 100%
rename from bin/toucan-render/CmdLineInline.h
rename to lib/toucan/CmdLineInline.h
diff --git a/lib/toucan/FFmpeg.cpp b/lib/toucan/FFmpeg.cpp
new file mode 100644
index 0000000..0fb1498
--- /dev/null
+++ b/lib/toucan/FFmpeg.cpp
@@ -0,0 +1,164 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Contributors to the toucan project.
+
+#include "FFmpeg.h"
+
+#include "Util.h"
+
+extern "C"
+{
+#include
+#include
+}
+
+#include
+#include
+
+namespace toucan
+{
+ namespace ffmpeg
+ {
+ AVRational swap(AVRational value)
+ {
+ return AVRational({ value.den, value.num });
+ }
+
+ std::vector getVideoExtensions()
+ {
+ return std::vector({ ".mov", ".mp4", ".m4v" });
+ }
+
+ bool hasVideoExtension(const std::string& value)
+ {
+ const std::vector extensions = getVideoExtensions();
+ const auto i = std::find(extensions.begin(), extensions.end(), value);
+ return i != extensions.end();
+ }
+
+ namespace
+ {
+ std::vector > _getVideoCodecs()
+ {
+ std::vector > out;
+ const AVCodec* avCodec = nullptr;
+ void* avCodecIterate = nullptr;
+ std::vector codecNames;
+ while ((avCodec = av_codec_iterate(&avCodecIterate)))
+ {
+ if (av_codec_is_encoder(avCodec) &&
+ AVMEDIA_TYPE_VIDEO == avcodec_get_type(avCodec->id))
+ {
+ out.push_back({ avCodec->id, avCodec->name });
+ }
+ }
+ return out;
+ }
+
+ const std::vector videoCodecStrings =
+ {
+ "MJPEG",
+ "V210",
+ "V308",
+ "V408",
+ "V410",
+ "AV1"
+ };
+
+ const std::vector videoCodecIds =
+ {
+ AV_CODEC_ID_MJPEG,
+ AV_CODEC_ID_V210,
+ AV_CODEC_ID_V308,
+ AV_CODEC_ID_V408,
+ AV_CODEC_ID_V410,
+ AV_CODEC_ID_AV1
+ };
+
+ const std::vector videoCodecProfiles =
+ {
+ AV_PROFILE_UNKNOWN,
+ AV_PROFILE_UNKNOWN,
+ AV_PROFILE_UNKNOWN,
+ AV_PROFILE_UNKNOWN,
+ AV_PROFILE_UNKNOWN,
+ AV_PROFILE_AV1_MAIN
+ };
+ }
+
+ std::vector getVideoCodecs()
+ {
+ std::vector out;
+ for (const auto& i : _getVideoCodecs())
+ {
+ for (size_t j = 0; j < videoCodecIds.size(); ++j)
+ {
+ if (i.first == videoCodecIds[j])
+ {
+ out.push_back(static_cast(j));
+ }
+ }
+ }
+ return out;
+ }
+
+ std::vector getVideoCodecStrings()
+ {
+ std::vector out;
+ for (const auto& i : getVideoCodecs())
+ {
+ out.push_back(toString(i));
+ }
+ return out;
+ }
+
+ std::string toString(VideoCodec value)
+ {
+ return videoCodecStrings[static_cast(value)];
+ }
+
+ void fromString(const std::string& s, VideoCodec& value)
+ {
+ const auto i = std::find(videoCodecStrings.begin(), videoCodecStrings.end(), s);
+ value = i != videoCodecStrings.end() ?
+ static_cast(i - videoCodecStrings.begin()) :
+ VideoCodec::First;
+ }
+
+ AVCodecID getVideoCodecId(VideoCodec value)
+ {
+ return videoCodecIds[static_cast(value)];
+ }
+
+ int getVideoCodecProfile(VideoCodec value)
+ {
+ return videoCodecProfiles[static_cast(value)];
+ }
+
+ std::string getErrorLabel(int r)
+ {
+ char buf[4096];
+ av_strerror(r, buf, 4096);
+ return std::string(buf);
+ }
+
+ void log(void*, int level, const char* fmt, va_list vl)
+ {
+ switch (level)
+ {
+ case AV_LOG_PANIC:
+ case AV_LOG_FATAL:
+ case AV_LOG_ERROR:
+ case AV_LOG_WARNING:
+ case AV_LOG_INFO:
+ {
+ char buf[4096];
+ vsnprintf(buf, 4096, fmt, vl);
+ std::cout << buf;
+ }
+ break;
+ case AV_LOG_VERBOSE:
+ default: break;
+ }
+ }
+ }
+}
diff --git a/lib/toucan/FFmpeg.h b/lib/toucan/FFmpeg.h
new file mode 100644
index 0000000..5f2207f
--- /dev/null
+++ b/lib/toucan/FFmpeg.h
@@ -0,0 +1,67 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Contributors to the toucan project.
+
+#pragma once
+
+#include
+#include
+
+extern "C"
+{
+#include
+#include
+#include
+}
+
+namespace toucan
+{
+ namespace ffmpeg
+ {
+ //! Swap the numerator and denominator.
+ AVRational swap(AVRational);
+
+ //! Get a list of supported video extensions.
+ std::vector getVideoExtensions();
+
+ //! Check if the given extension is supported.
+ bool hasVideoExtension(const std::string&);
+
+ //! Video codecs.
+ enum class VideoCodec
+ {
+ MJPEG,
+ V210,
+ V308,
+ V408,
+ V410,
+ AV1,
+
+ Count,
+ First = MJPEG
+ };
+
+ //! Get a list of video codecs.
+ std::vector getVideoCodecs();
+
+ //! Get a list of video codec strings.
+ std::vector getVideoCodecStrings();
+
+ //! Convert a video codec to a string.
+ std::string toString(VideoCodec);
+
+ //! Convert a string to a video codec.
+ void fromString(const std::string&, VideoCodec&);
+
+ //! Get a video codec ID.
+ AVCodecID getVideoCodecId(VideoCodec);
+
+ //! Get a video codec profile.
+ int getVideoCodecProfile(VideoCodec);
+
+ //! FFmpeg log callback.
+ void log(void*, int level, const char* fmt, va_list vl);
+
+ //! Get an error label.
+ std::string getErrorLabel(int);
+ }
+}
diff --git a/lib/toucan/FFmpegRead.cpp b/lib/toucan/FFmpegRead.cpp
index b368bd2..fc5a940 100644
--- a/lib/toucan/FFmpegRead.cpp
+++ b/lib/toucan/FFmpegRead.cpp
@@ -16,587 +16,559 @@ extern "C"
namespace toucan
{
- namespace
+ namespace ffmpeg
{
- const size_t avIOContextBufferSize = 4096;
-
- void logCallback(void*, int level, const char* fmt, va_list vl)
+ namespace
{
- switch (level)
+ const size_t avIOContextBufferSize = 4096;
+
+ class Packet
{
- case AV_LOG_PANIC:
- case AV_LOG_FATAL:
- case AV_LOG_ERROR:
- case AV_LOG_WARNING:
- case AV_LOG_INFO:
+ public:
+ Packet()
{
- char buf[4096];
- vsnprintf(buf, 4096, fmt, vl);
- std::cout << buf;
+ p = av_packet_alloc();
}
- break;
- case AV_LOG_VERBOSE:
- default: break;
- }
- }
- AVRational swap(AVRational value)
- {
- return AVRational({ value.den, value.num });
+ ~Packet()
+ {
+ av_packet_free(&p);
+ }
+
+ AVPacket* p = nullptr;
+ };
}
- class Packet
+ Read::Read(
+ const std::filesystem::path& path,
+ const MemoryReference& memoryReference) :
+ _path(path),
+ _memoryReference(memoryReference)
{
- public:
- Packet()
- {
- p = av_packet_alloc();
- }
-
- ~Packet()
- {
- av_packet_free(&p);
- }
-
- AVPacket* p = nullptr;
- };
+ av_log_set_level(AV_LOG_QUIET);
+ //av_log_set_level(AV_LOG_VERBOSE);
+ //av_log_set_callback(log);
- size_t getByteCount(const OIIO::ImageSpec& spec)
- {
- size_t type = 0;
- switch (spec.format.basetype)
+ if (memoryReference.isValid())
{
+ _avFormatContext = avformat_alloc_context();
+ if (!_avFormatContext)
+ {
+ throw std::runtime_error("Cannot allocate format context");
+ }
- }
- return spec.width * spec.height * type;
- }
- }
-
- FFmpegRead::FFmpegRead(
- const std::filesystem::path& path,
- const MemoryReference& memoryReference) :
- _path(path),
- _memoryReference(memoryReference)
- {
- av_log_set_level(AV_LOG_QUIET);
- //av_log_set_level(AV_LOG_VERBOSE);
- //av_log_set_callback(logCallback);
+ _avIOBufferData = AVIOBufferData(
+ reinterpret_cast(memoryReference.getData()),
+ memoryReference.getSize());
+ _avIOContextBuffer = static_cast(av_malloc(avIOContextBufferSize));
+ _avIOContext = avio_alloc_context(
+ _avIOContextBuffer,
+ avIOContextBufferSize,
+ 0,
+ &_avIOBufferData,
+ &_avIOBufferRead,
+ nullptr,
+ &_avIOBufferSeek);
+ if (!_avIOContext)
+ {
+ throw std::runtime_error("Cannot allocate I/O context");
+ }
- if (memoryReference.isValid())
- {
- _avFormatContext = avformat_alloc_context();
- if (!_avFormatContext)
- {
- throw std::runtime_error("Cannot allocate format context");
+ _avFormatContext->pb = _avIOContext;
}
- _avIOBufferData = AVIOBufferData(
- reinterpret_cast(memoryReference.getData()),
- memoryReference.getSize());
- _avIOContextBuffer = static_cast(av_malloc(avIOContextBufferSize));
- _avIOContext = avio_alloc_context(
- _avIOContextBuffer,
- avIOContextBufferSize,
- 0,
- &_avIOBufferData,
- &_avIOBufferRead,
+ const std::string fileName = path.string();
+ int r = avformat_open_input(
+ &_avFormatContext,
+ !_avFormatContext ? fileName.c_str() : nullptr,
nullptr,
- &_avIOBufferSeek);
- if (!_avIOContext)
+ nullptr);
+ if (r < 0 || !_avFormatContext)
{
- throw std::runtime_error("Cannot allocate I/O context");
+ throw std::runtime_error("Cannot open file");
}
- _avFormatContext->pb = _avIOContext;
- }
-
- const std::string fileName = path.string();
- int r = avformat_open_input(
- &_avFormatContext,
- !_avFormatContext ? fileName.c_str() : nullptr,
- nullptr,
- nullptr);
- if (r < 0 || !_avFormatContext)
- {
- throw std::runtime_error("Cannot open file");
- }
-
- r = avformat_find_stream_info(_avFormatContext, nullptr);
- if (r < 0)
- {
- throw std::runtime_error("Cannot find stream info");
- }
- for (unsigned int i = 0; i < _avFormatContext->nb_streams; ++i)
- {
- //av_dump_format(_avFormatContext, 0, fileName.c_str(), 0);
- if (AVMEDIA_TYPE_VIDEO == _avFormatContext->streams[i]->codecpar->codec_type &&
- AV_DISPOSITION_DEFAULT == _avFormatContext->streams[i]->disposition)
+ r = avformat_find_stream_info(_avFormatContext, nullptr);
+ if (r < 0)
{
- _avStream = i;
- break;
+ throw std::runtime_error("Cannot find stream info");
}
- }
- if (-1 == _avStream)
- {
for (unsigned int i = 0; i < _avFormatContext->nb_streams; ++i)
{
- if (AVMEDIA_TYPE_VIDEO == _avFormatContext->streams[i]->codecpar->codec_type)
+ //av_dump_format(_avFormatContext, 0, fileName.c_str(), 0);
+ if (AVMEDIA_TYPE_VIDEO == _avFormatContext->streams[i]->codecpar->codec_type &&
+ AV_DISPOSITION_DEFAULT == _avFormatContext->streams[i]->disposition)
{
_avStream = i;
break;
}
}
- }
-
- int dataStream = -1;
- for (unsigned int i = 0; i < _avFormatContext->nb_streams; ++i)
- {
- if (AVMEDIA_TYPE_DATA == _avFormatContext->streams[i]->codecpar->codec_type &&
- AV_DISPOSITION_DEFAULT == _avFormatContext->streams[i]->disposition)
+ if (-1 == _avStream)
{
- dataStream = i;
- break;
+ for (unsigned int i = 0; i < _avFormatContext->nb_streams; ++i)
+ {
+ if (AVMEDIA_TYPE_VIDEO == _avFormatContext->streams[i]->codecpar->codec_type)
+ {
+ _avStream = i;
+ break;
+ }
+ }
}
- }
- if (-1 == dataStream)
- {
+
+ int dataStream = -1;
for (unsigned int i = 0; i < _avFormatContext->nb_streams; ++i)
{
- if (AVMEDIA_TYPE_DATA == _avFormatContext->streams[i]->codecpar->codec_type)
+ if (AVMEDIA_TYPE_DATA == _avFormatContext->streams[i]->codecpar->codec_type &&
+ AV_DISPOSITION_DEFAULT == _avFormatContext->streams[i]->disposition)
{
dataStream = i;
break;
}
}
- }
- std::string timecode;
- if (dataStream != -1)
- {
- AVDictionaryEntry* tag = nullptr;
- while ((tag = av_dict_get(
- _avFormatContext->streams[dataStream]->metadata,
- "",
- tag,
- AV_DICT_IGNORE_SUFFIX)))
+ if (-1 == dataStream)
{
- if ("timecode" == toLower(tag->key))
+ for (unsigned int i = 0; i < _avFormatContext->nb_streams; ++i)
{
- timecode = tag->value;
- break;
+ if (AVMEDIA_TYPE_DATA == _avFormatContext->streams[i]->codecpar->codec_type)
+ {
+ dataStream = i;
+ break;
+ }
}
}
- }
-
- if (_avStream != -1)
- {
- //av_dump_format(_avFormatContext, _avStream, fileName.c_str(), 0);
-
- auto avVideoStream = _avFormatContext->streams[_avStream];
- auto avVideoCodecParameters = avVideoStream->codecpar;
- auto avVideoCodec = avcodec_find_decoder(avVideoCodecParameters->codec_id);
- if (!avVideoCodec)
- {
- throw std::runtime_error("No video codec found");
- }
- _avCodecParameters[_avStream] = avcodec_parameters_alloc();
- if (!_avCodecParameters[_avStream])
- {
- throw std::runtime_error("Cannot allocate parameters");
- }
- avcodec_parameters_copy(_avCodecParameters[_avStream], avVideoCodecParameters);
- _avCodecContext[_avStream] = avcodec_alloc_context3(avVideoCodec);
- if (!_avCodecParameters[_avStream])
- {
- throw std::runtime_error("Cannot allocate context");
- }
- avcodec_parameters_to_context(_avCodecContext[_avStream], _avCodecParameters[_avStream]);
- _avCodecContext[_avStream]->thread_count = 0;
- _avCodecContext[_avStream]->thread_type = FF_THREAD_FRAME;
- r = avcodec_open2(_avCodecContext[_avStream], avVideoCodec, 0);
- if (r < 0)
+ std::string timecode;
+ if (dataStream != -1)
{
- throw std::runtime_error("Cannot open stream");
+ AVDictionaryEntry* tag = nullptr;
+ while ((tag = av_dict_get(
+ _avFormatContext->streams[dataStream]->metadata,
+ "",
+ tag,
+ AV_DICT_IGNORE_SUFFIX)))
+ {
+ if ("timecode" == toLower(tag->key))
+ {
+ timecode = tag->value;
+ break;
+ }
+ }
}
- int width = _avCodecParameters[_avStream]->width;
- int height = _avCodecParameters[_avStream]->height;
- double pixelAspectRatio = 1.0;
- if (_avCodecParameters[_avStream]->sample_aspect_ratio.den > 0 &&
- _avCodecParameters[_avStream]->sample_aspect_ratio.num > 0)
+ if (_avStream != -1)
{
- pixelAspectRatio = av_q2d(_avCodecParameters[_avStream]->sample_aspect_ratio);
- }
+ //av_dump_format(_avFormatContext, _avStream, fileName.c_str(), 0);
- _avInputPixelFormat = static_cast(_avCodecParameters[_avStream]->format);
- int nchannels = 0;
- OIIO::TypeDesc format = OIIO::TypeDesc::UNKNOWN;
- switch (_avInputPixelFormat)
- {
- case AV_PIX_FMT_RGB24:
- _avOutputPixelFormat = _avInputPixelFormat;
- nchannels = 3;
- format = OIIO::TypeUInt8;
- break;
- case AV_PIX_FMT_GRAY8:
- _avOutputPixelFormat = _avInputPixelFormat;
- nchannels = 1;
- format = OIIO::TypeUInt8;
- break;
- case AV_PIX_FMT_RGBA:
- _avOutputPixelFormat = _avInputPixelFormat;
- nchannels = 4;
- format = OIIO::TypeUInt8;
- break;
- case AV_PIX_FMT_YUV420P:
- case AV_PIX_FMT_YUV422P:
- case AV_PIX_FMT_YUV444P:
- _avOutputPixelFormat = AV_PIX_FMT_RGB24;
- nchannels = 3;
- format = OIIO::TypeUInt8;
- break;
- case AV_PIX_FMT_YUV420P10BE:
- case AV_PIX_FMT_YUV420P10LE:
- case AV_PIX_FMT_YUV420P12BE:
- case AV_PIX_FMT_YUV420P12LE:
- case AV_PIX_FMT_YUV420P16BE:
- case AV_PIX_FMT_YUV420P16LE:
- case AV_PIX_FMT_YUV422P10BE:
- case AV_PIX_FMT_YUV422P10LE:
- case AV_PIX_FMT_YUV422P12BE:
- case AV_PIX_FMT_YUV422P12LE:
- case AV_PIX_FMT_YUV422P16BE:
- case AV_PIX_FMT_YUV422P16LE:
- case AV_PIX_FMT_YUV444P10BE:
- case AV_PIX_FMT_YUV444P10LE:
- case AV_PIX_FMT_YUV444P12BE:
- case AV_PIX_FMT_YUV444P12LE:
- case AV_PIX_FMT_YUV444P16BE:
- case AV_PIX_FMT_YUV444P16LE:
- _avOutputPixelFormat = AV_PIX_FMT_RGB48;
- nchannels = 3;
- format = OIIO::TypeUInt16;
- break;
- case AV_PIX_FMT_YUVA420P:
- case AV_PIX_FMT_YUVA422P:
- case AV_PIX_FMT_YUVA444P:
- _avOutputPixelFormat = AV_PIX_FMT_RGBA;
- nchannels = 4;
- format = OIIO::TypeUInt8;
- break;
- case AV_PIX_FMT_YUVA444P10BE:
- case AV_PIX_FMT_YUVA444P10LE:
- case AV_PIX_FMT_YUVA444P12BE:
- case AV_PIX_FMT_YUVA444P12LE:
- case AV_PIX_FMT_YUVA444P16BE:
- case AV_PIX_FMT_YUVA444P16LE:
- _avOutputPixelFormat = AV_PIX_FMT_RGBA64;
- nchannels = 4;
- format = OIIO::TypeUInt16;
- break;
- default:
- _avOutputPixelFormat = AV_PIX_FMT_RGB24;
- nchannels = 3;
- format = OIIO::TypeUInt8;
- break;
- }
- _spec = OIIO::ImageSpec(width, height, nchannels, format);
+ auto avVideoStream = _avFormatContext->streams[_avStream];
+ auto avVideoCodecParameters = avVideoStream->codecpar;
+ auto avVideoCodec = avcodec_find_decoder(avVideoCodecParameters->codec_id);
+ if (!avVideoCodec)
+ {
+ throw std::runtime_error("No video codec found");
+ }
+ _avCodecParameters[_avStream] = avcodec_parameters_alloc();
+ if (!_avCodecParameters[_avStream])
+ {
+ throw std::runtime_error("Cannot allocate parameters");
+ }
+ avcodec_parameters_copy(_avCodecParameters[_avStream], avVideoCodecParameters);
+ _avCodecContext[_avStream] = avcodec_alloc_context3(avVideoCodec);
+ if (!_avCodecParameters[_avStream])
+ {
+ throw std::runtime_error("Cannot allocate context");
+ }
+ avcodec_parameters_to_context(_avCodecContext[_avStream], _avCodecParameters[_avStream]);
+ _avCodecContext[_avStream]->thread_count = 0;
+ _avCodecContext[_avStream]->thread_type = FF_THREAD_FRAME;
+ r = avcodec_open2(_avCodecContext[_avStream], avVideoCodec, 0);
+ if (r < 0)
+ {
+ throw std::runtime_error("Cannot open stream");
+ }
- _avSpeed = av_guess_frame_rate(_avFormatContext, avVideoStream, nullptr);
- const double speed = av_q2d(_avSpeed);
+ int width = _avCodecParameters[_avStream]->width;
+ int height = _avCodecParameters[_avStream]->height;
+ double pixelAspectRatio = 1.0;
+ if (_avCodecParameters[_avStream]->sample_aspect_ratio.den > 0 &&
+ _avCodecParameters[_avStream]->sample_aspect_ratio.num > 0)
+ {
+ pixelAspectRatio = av_q2d(_avCodecParameters[_avStream]->sample_aspect_ratio);
+ }
- std::size_t frameCount = 0;
- if (avVideoStream->nb_frames > 0)
- {
- frameCount = avVideoStream->nb_frames;
- }
- else if (avVideoStream->duration != AV_NOPTS_VALUE)
- {
- frameCount = av_rescale_q(
- avVideoStream->duration,
- avVideoStream->time_base,
- swap(avVideoStream->r_frame_rate));
- }
- else if (_avFormatContext->duration != AV_NOPTS_VALUE)
- {
- frameCount = av_rescale_q(
- _avFormatContext->duration,
- av_get_time_base_q(),
- swap(avVideoStream->r_frame_rate));
- }
+ _avInputPixelFormat = static_cast(_avCodecParameters[_avStream]->format);
+ int nchannels = 0;
+ OIIO::TypeDesc format = OIIO::TypeDesc::UNKNOWN;
+ switch (_avInputPixelFormat)
+ {
+ case AV_PIX_FMT_RGB24:
+ _avOutputPixelFormat = _avInputPixelFormat;
+ nchannels = 3;
+ format = OIIO::TypeUInt8;
+ break;
+ case AV_PIX_FMT_GRAY8:
+ _avOutputPixelFormat = _avInputPixelFormat;
+ nchannels = 1;
+ format = OIIO::TypeUInt8;
+ break;
+ case AV_PIX_FMT_RGBA:
+ _avOutputPixelFormat = _avInputPixelFormat;
+ nchannels = 4;
+ format = OIIO::TypeUInt8;
+ break;
+ case AV_PIX_FMT_YUV420P:
+ case AV_PIX_FMT_YUV422P:
+ case AV_PIX_FMT_YUV444P:
+ _avOutputPixelFormat = AV_PIX_FMT_RGB24;
+ nchannels = 3;
+ format = OIIO::TypeUInt8;
+ break;
+ case AV_PIX_FMT_YUV420P10BE:
+ case AV_PIX_FMT_YUV420P10LE:
+ case AV_PIX_FMT_YUV420P12BE:
+ case AV_PIX_FMT_YUV420P12LE:
+ case AV_PIX_FMT_YUV420P16BE:
+ case AV_PIX_FMT_YUV420P16LE:
+ case AV_PIX_FMT_YUV422P10BE:
+ case AV_PIX_FMT_YUV422P10LE:
+ case AV_PIX_FMT_YUV422P12BE:
+ case AV_PIX_FMT_YUV422P12LE:
+ case AV_PIX_FMT_YUV422P16BE:
+ case AV_PIX_FMT_YUV422P16LE:
+ case AV_PIX_FMT_YUV444P10BE:
+ case AV_PIX_FMT_YUV444P10LE:
+ case AV_PIX_FMT_YUV444P12BE:
+ case AV_PIX_FMT_YUV444P12LE:
+ case AV_PIX_FMT_YUV444P16BE:
+ case AV_PIX_FMT_YUV444P16LE:
+ _avOutputPixelFormat = AV_PIX_FMT_RGB48;
+ nchannels = 3;
+ format = OIIO::TypeUInt16;
+ break;
+ case AV_PIX_FMT_YUVA420P:
+ case AV_PIX_FMT_YUVA422P:
+ case AV_PIX_FMT_YUVA444P:
+ _avOutputPixelFormat = AV_PIX_FMT_RGBA;
+ nchannels = 4;
+ format = OIIO::TypeUInt8;
+ break;
+ case AV_PIX_FMT_YUVA444P10BE:
+ case AV_PIX_FMT_YUVA444P10LE:
+ case AV_PIX_FMT_YUVA444P12BE:
+ case AV_PIX_FMT_YUVA444P12LE:
+ case AV_PIX_FMT_YUVA444P16BE:
+ case AV_PIX_FMT_YUVA444P16LE:
+ _avOutputPixelFormat = AV_PIX_FMT_RGBA64;
+ nchannels = 4;
+ format = OIIO::TypeUInt16;
+ break;
+ default:
+ _avOutputPixelFormat = AV_PIX_FMT_RGB24;
+ nchannels = 3;
+ format = OIIO::TypeUInt8;
+ break;
+ }
+ _spec = OIIO::ImageSpec(width, height, nchannels, format);
- AVDictionaryEntry* tag = nullptr;
- while ((tag = av_dict_get(_avFormatContext->metadata, "", tag, AV_DICT_IGNORE_SUFFIX)))
- {
- const std::string key(tag->key);
- const std::string value(tag->value);
- if ("timecode" == toLower(key))
+ _avSpeed = av_guess_frame_rate(_avFormatContext, avVideoStream, nullptr);
+ const double speed = av_q2d(_avSpeed);
+
+ std::size_t frameCount = 0;
+ if (avVideoStream->nb_frames > 0)
{
- timecode = value;
+ frameCount = avVideoStream->nb_frames;
+ }
+ else if (avVideoStream->duration != AV_NOPTS_VALUE)
+ {
+ frameCount = av_rescale_q(
+ avVideoStream->duration,
+ avVideoStream->time_base,
+ swap(avVideoStream->r_frame_rate));
+ }
+ else if (_avFormatContext->duration != AV_NOPTS_VALUE)
+ {
+ frameCount = av_rescale_q(
+ _avFormatContext->duration,
+ av_get_time_base_q(),
+ swap(avVideoStream->r_frame_rate));
}
- }
- OTIO_NS::RationalTime startTime(0.0, speed);
- if (!timecode.empty())
- {
- opentime::ErrorStatus errorStatus;
- const OTIO_NS::RationalTime time = OTIO_NS::RationalTime::from_timecode(
- timecode,
- speed,
- &errorStatus);
- if (!opentime::is_error(errorStatus))
+ AVDictionaryEntry* tag = nullptr;
+ while ((tag = av_dict_get(_avFormatContext->metadata, "", tag, AV_DICT_IGNORE_SUFFIX)))
{
- startTime = time.floor();
+ const std::string key(tag->key);
+ const std::string value(tag->value);
+ if ("timecode" == toLower(key))
+ {
+ timecode = value;
+ }
}
+
+ OTIO_NS::RationalTime startTime(0.0, speed);
+ if (!timecode.empty())
+ {
+ opentime::ErrorStatus errorStatus;
+ const OTIO_NS::RationalTime time = OTIO_NS::RationalTime::from_timecode(
+ timecode,
+ speed,
+ &errorStatus);
+ if (!opentime::is_error(errorStatus))
+ {
+ startTime = time.floor();
+ }
+ }
+ _timeRange = OTIO_NS::TimeRange(
+ startTime,
+ OTIO_NS::RationalTime(frameCount, speed));
+ _currentTime = startTime;
+
+ _avFrame = av_frame_alloc();
+ if (!_avFrame)
+ {
+ throw std::runtime_error("Cannot allocate frame");
+ }
+ _avFrame2 = av_frame_alloc();
+ if (!_avFrame2)
+ {
+ throw std::runtime_error("Cannot allocate frame");
+ }
+ //! \bug These fields need to be filled out for
+ //! sws_scale_frame()?
+ _avFrame2->format = _avOutputPixelFormat;
+ _avFrame2->width = width;
+ _avFrame2->height = height;
+ _avFrame2->buf[0] = av_buffer_alloc(_spec.image_bytes());
+
+ _swsContext = sws_alloc_context();
+ if (!_swsContext)
+ {
+ throw std::runtime_error("Cannot allocate context");
+ }
+ av_opt_set_defaults(_swsContext);
+ int r = av_opt_set_int(_swsContext, "srcw", _avCodecParameters[_avStream]->width, AV_OPT_SEARCH_CHILDREN);
+ r = av_opt_set_int(_swsContext, "srch", _avCodecParameters[_avStream]->height, AV_OPT_SEARCH_CHILDREN);
+ r = av_opt_set_int(_swsContext, "src_format", _avInputPixelFormat, AV_OPT_SEARCH_CHILDREN);
+ r = av_opt_set_int(_swsContext, "dstw", _avCodecParameters[_avStream]->width, AV_OPT_SEARCH_CHILDREN);
+ r = av_opt_set_int(_swsContext, "dsth", _avCodecParameters[_avStream]->height, AV_OPT_SEARCH_CHILDREN);
+ r = av_opt_set_int(_swsContext, "dst_format", _avOutputPixelFormat, AV_OPT_SEARCH_CHILDREN);
+ r = av_opt_set_int(_swsContext, "sws_flags", SWS_FAST_BILINEAR, AV_OPT_SEARCH_CHILDREN);
+ r = av_opt_set_int(_swsContext, "threads", 0, AV_OPT_SEARCH_CHILDREN);
+ r = sws_init_context(_swsContext, nullptr, nullptr);
+ if (r < 0)
+ {
+ throw std::runtime_error("Cannot initialize sws context");
+ }
+
+ const int* inTable = nullptr;
+ int inFull = 0;
+ const int* outTable = nullptr;
+ int outFull = 0;
+ int brightness = 0;
+ int contrast = 0;
+ int saturation = 0;
+ r = sws_getColorspaceDetails(
+ _swsContext,
+ (int**)&inTable,
+ &inFull,
+ (int**)&outTable,
+ &outFull,
+ &brightness,
+ &contrast,
+ &saturation);
+
+ AVColorSpace colorSpace = _avCodecParameters[_avStream]->color_space;
+ if (AVCOL_SPC_UNSPECIFIED == colorSpace)
+ {
+ colorSpace = AVCOL_SPC_BT709;
+ }
+ inFull = 1;
+ outFull = 1;
+
+ r = sws_setColorspaceDetails(
+ _swsContext,
+ sws_getCoefficients(colorSpace),
+ inFull,
+ sws_getCoefficients(AVCOL_SPC_BT709),
+ outFull,
+ brightness,
+ contrast,
+ saturation);
}
- _timeRange = OTIO_NS::TimeRange(
- startTime,
- OTIO_NS::RationalTime(frameCount, speed));
- _currentTime = startTime;
+ }
- _avFrame = av_frame_alloc();
- if (!_avFrame)
+ Read::~Read()
+ {
+ if (_swsContext)
{
- throw std::runtime_error("Cannot allocate frame");
+ sws_freeContext(_swsContext);
}
- _avFrame2 = av_frame_alloc();
- if (!_avFrame2)
+ if (_avFrame2)
{
- throw std::runtime_error("Cannot allocate frame");
+ av_frame_free(&_avFrame2);
}
- //! \bug These fields need to be filled out for
- //! sws_scale_frame()?
- _avFrame2->format = _avOutputPixelFormat;
- _avFrame2->width = width;
- _avFrame2->height = height;
- _avFrame2->buf[0] = av_buffer_alloc(_spec.image_bytes());
-
- _swsContext = sws_alloc_context();
- if (!_swsContext)
+ if (_avFrame)
{
- throw std::runtime_error("Cannot allocate context");
+ av_frame_free(&_avFrame);
}
- av_opt_set_defaults(_swsContext);
- int r = av_opt_set_int(_swsContext, "srcw", _avCodecParameters[_avStream]->width, AV_OPT_SEARCH_CHILDREN);
- r = av_opt_set_int(_swsContext, "srch", _avCodecParameters[_avStream]->height, AV_OPT_SEARCH_CHILDREN);
- r = av_opt_set_int(_swsContext, "src_format", _avInputPixelFormat, AV_OPT_SEARCH_CHILDREN);
- r = av_opt_set_int(_swsContext, "dstw", _avCodecParameters[_avStream]->width, AV_OPT_SEARCH_CHILDREN);
- r = av_opt_set_int(_swsContext, "dsth", _avCodecParameters[_avStream]->height, AV_OPT_SEARCH_CHILDREN);
- r = av_opt_set_int(_swsContext, "dst_format", _avOutputPixelFormat, AV_OPT_SEARCH_CHILDREN);
- r = av_opt_set_int(_swsContext, "sws_flags", SWS_FAST_BILINEAR, AV_OPT_SEARCH_CHILDREN);
- r = av_opt_set_int(_swsContext, "threads", 0, AV_OPT_SEARCH_CHILDREN);
- r = sws_init_context(_swsContext, nullptr, nullptr);
- if (r < 0)
+ for (auto i : _avCodecContext)
{
- throw std::runtime_error("Cannot initialize sws context");
+ avcodec_close(i.second);
+ avcodec_free_context(&i.second);
}
-
- const int* inTable = nullptr;
- int inFull = 0;
- const int* outTable = nullptr;
- int outFull = 0;
- int brightness = 0;
- int contrast = 0;
- int saturation = 0;
- r = sws_getColorspaceDetails(
- _swsContext,
- (int**)&inTable,
- &inFull,
- (int**)&outTable,
- &outFull,
- &brightness,
- &contrast,
- &saturation);
-
- AVColorSpace colorSpace = _avCodecParameters[_avStream]->color_space;
- if (AVCOL_SPC_UNSPECIFIED == colorSpace)
+ for (auto i : _avCodecParameters)
+ {
+ avcodec_parameters_free(&i.second);
+ }
+ if (_avIOContext)
{
- colorSpace = AVCOL_SPC_BT709;
+ avio_context_free(&_avIOContext);
+ }
+ //! \bug Free'd by avio_context_free()?
+ //if (_avIOContextBuffer)
+ //{
+ // av_free(_avIOContextBuffer);
+ //}
+ if (_avFormatContext)
+ {
+ avformat_close_input(&_avFormatContext);
}
- inFull = 1;
- outFull = 1;
-
- r = sws_setColorspaceDetails(
- _swsContext,
- sws_getCoefficients(colorSpace),
- inFull,
- sws_getCoefficients(AVCOL_SPC_BT709),
- outFull,
- brightness,
- contrast,
- saturation);
}
- }
- FFmpegRead::~FFmpegRead()
- {
- if (_swsContext)
+ const OIIO::ImageSpec& Read::getSpec()
{
- sws_freeContext(_swsContext);
+ return _spec;
}
- if (_avFrame2)
- {
- av_frame_free(&_avFrame2);
- }
- if (_avFrame)
- {
- av_frame_free(&_avFrame);
- }
- for (auto i : _avCodecContext)
- {
- avcodec_close(i.second);
- avcodec_free_context(&i.second);
- }
- for (auto i : _avCodecParameters)
- {
- avcodec_parameters_free(&i.second);
- }
- if (_avIOContext)
- {
- avio_context_free(&_avIOContext);
- }
- //! \bug Free'd by avio_context_free()?
- //if (_avIOContextBuffer)
- //{
- // av_free(_avIOContextBuffer);
- //}
- if (_avFormatContext)
- {
- avformat_close_input(&_avFormatContext);
- }
- }
- const OIIO::ImageSpec& FFmpegRead::getSpec()
- {
- return _spec;
- }
-
- const OTIO_NS::TimeRange& FFmpegRead::getTimeRange() const
- {
- return _timeRange;
- }
-
- OIIO::ImageBuf FFmpegRead::getImage(const OTIO_NS::RationalTime& time)
- {
- if (time != _currentTime)
+ const OTIO_NS::TimeRange& Read::getTimeRange() const
{
- _seek(time);
+ return _timeRange;
}
- return _read();
- }
- void FFmpegRead::_seek(const OTIO_NS::RationalTime& time)
- {
- if (_avStream != -1)
+ OIIO::ImageBuf Read::getImage(const OTIO_NS::RationalTime& time)
{
- avcodec_flush_buffers(_avCodecContext[_avStream]);
- if (av_seek_frame(
- _avFormatContext,
- _avStream,
- av_rescale_q(
- time.value() - _timeRange.start_time().value(),
- swap(_avSpeed),
- _avFormatContext->streams[_avStream]->time_base),
- AVSEEK_FLAG_BACKWARD) < 0)
+ if (time != _currentTime)
{
- //! \todo How should this be handled?
+ _seek(time);
}
- _currentTime = time;
+ return _read();
}
- _eof = false;
- }
- OIIO::ImageBuf FFmpegRead::_read()
- {
- OIIO::ImageBuf out;
- if (_avStream != -1)
+ void Read::_seek(const OTIO_NS::RationalTime& time)
{
- Packet packet;
- int decoding = 0;
- while (0 == decoding)
+ if (_avStream != -1)
{
- if (!_eof)
+ avcodec_flush_buffers(_avCodecContext[_avStream]);
+ if (av_seek_frame(
+ _avFormatContext,
+ _avStream,
+ av_rescale_q(
+ time.value() - _timeRange.start_time().value(),
+ swap(_avSpeed),
+ _avFormatContext->streams[_avStream]->time_base),
+ AVSEEK_FLAG_BACKWARD) < 0)
{
- decoding = av_read_frame(_avFormatContext, packet.p);
- if (AVERROR_EOF == decoding)
- {
- _eof = true;
- decoding = 0;
- }
- else if (decoding < 0)
- {
- //! \todo How should this be handled?
- break;
- }
+ //! \todo How should this be handled?
}
- if ((_eof && _avStream != -1) || (_avStream == packet.p->stream_index))
+ _currentTime = time;
+ }
+ _eof = false;
+ }
+
+ OIIO::ImageBuf Read::_read()
+ {
+ OIIO::ImageBuf out;
+ if (_avStream != -1)
+ {
+ Packet packet;
+ int decoding = 0;
+ while (0 == decoding)
{
- decoding = avcodec_send_packet(
- _avCodecContext[_avStream],
- _eof ? nullptr : packet.p);
- if (AVERROR_EOF == decoding)
+ if (!_eof)
{
- decoding = 0;
- }
- else if (decoding < 0)
- {
- //! \todo How should this be handled?
- break;
+ decoding = av_read_frame(_avFormatContext, packet.p);
+ if (AVERROR_EOF == decoding)
+ {
+ _eof = true;
+ decoding = 0;
+ }
+ else if (decoding < 0)
+ {
+ //! \todo How should this be handled?
+ break;
+ }
}
-
- while (0 == decoding)
+ if ((_eof && _avStream != -1) || (_avStream == packet.p->stream_index))
{
- decoding = avcodec_receive_frame(_avCodecContext[_avStream], _avFrame);
- if (decoding < 0)
+ decoding = avcodec_send_packet(
+ _avCodecContext[_avStream],
+ _eof ? nullptr : packet.p);
+ if (AVERROR_EOF == decoding)
+ {
+ decoding = 0;
+ }
+ else if (decoding < 0)
{
+ //! \todo How should this be handled?
break;
}
- const int64_t timestamp = _avFrame->pts != AV_NOPTS_VALUE ? _avFrame->pts : _avFrame->pkt_dts;
- const OTIO_NS::RationalTime frameTime(
- _timeRange.start_time().value() +
- av_rescale_q(
- timestamp,
- _avFormatContext->streams[_avStream]->time_base,
- swap(_avFormatContext->streams[_avStream]->r_frame_rate)),
- _timeRange.duration().rate());
-
- if (frameTime >= _currentTime)
+ while (0 == decoding)
{
- out = OIIO::ImageBuf(_spec);
-
- av_image_fill_arrays(
- _avFrame2->data,
- _avFrame2->linesize,
- (const uint8_t*)out.localpixels(),
- _avOutputPixelFormat,
- _spec.width,
- _spec.height,
- 1);
- sws_scale_frame(_swsContext, _avFrame2, _avFrame);
-
- _currentTime += OTIO_NS::RationalTime(1.0, _timeRange.duration().rate());
+ decoding = avcodec_receive_frame(_avCodecContext[_avStream], _avFrame);
+ if (decoding < 0)
+ {
+ break;
+ }
+ const int64_t timestamp = _avFrame->pts != AV_NOPTS_VALUE ? _avFrame->pts : _avFrame->pkt_dts;
+
+ const OTIO_NS::RationalTime frameTime(
+ _timeRange.start_time().value() +
+ av_rescale_q(
+ timestamp,
+ _avFormatContext->streams[_avStream]->time_base,
+ swap(_avFormatContext->streams[_avStream]->r_frame_rate)),
+ _timeRange.duration().rate());
+
+ if (frameTime >= _currentTime)
+ {
+ out = OIIO::ImageBuf(_spec);
+
+ av_image_fill_arrays(
+ _avFrame2->data,
+ _avFrame2->linesize,
+ (const uint8_t*)out.localpixels(),
+ _avOutputPixelFormat,
+ _spec.width,
+ _spec.height,
+ 1);
+ sws_scale_frame(_swsContext, _avFrame2, _avFrame);
+
+ _currentTime += OTIO_NS::RationalTime(1.0, _timeRange.duration().rate());
+
+ decoding = 1;
+ break;
+ }
+ }
- decoding = 1;
+ if (AVERROR(EAGAIN) == decoding)
+ {
+ decoding = 0;
+ }
+ else if (AVERROR_EOF == decoding)
+ {
+ break;
+ }
+ else if (decoding < 0)
+ {
+ //! \todo How should this be handled?
+ break;
+ }
+ else if (1 == decoding)
+ {
break;
}
}
-
- if (AVERROR(EAGAIN) == decoding)
- {
- decoding = 0;
- }
- else if (AVERROR_EOF == decoding)
- {
- break;
- }
- else if (decoding < 0)
- {
- //! \todo How should this be handled?
- break;
- }
- else if (1 == decoding)
+ if (packet.p->buf)
{
- break;
+ av_packet_unref(packet.p);
}
}
if (packet.p->buf)
@@ -604,56 +576,54 @@ namespace toucan
av_packet_unref(packet.p);
}
}
- if (packet.p->buf)
- {
- av_packet_unref(packet.p);
- }
+ return out;
}
- return out;
- }
-
- FFmpegRead::AVIOBufferData::AVIOBufferData()
- {}
- FFmpegRead::AVIOBufferData::AVIOBufferData(const uint8_t* data, size_t size) :
- data(data),
- size(size)
- {}
+ Read::AVIOBufferData::AVIOBufferData()
+ {
+ }
- int FFmpegRead::_avIOBufferRead(void* opaque, uint8_t* buf, int bufSize)
- {
- AVIOBufferData* bufferData = static_cast(opaque);
-
- const int64_t remaining = bufferData->size - bufferData->offset;
- int bufSizeClamped = std::min(std::max(
- static_cast(bufSize),
- static_cast(0)),
- remaining);
- if (!bufSizeClamped)
+ Read::AVIOBufferData::AVIOBufferData(const uint8_t* data, size_t size) :
+ data(data),
+ size(size)
{
- return AVERROR_EOF;
}
- memcpy(buf, bufferData->data + bufferData->offset, bufSizeClamped);
- bufferData->offset += bufSizeClamped;
+ int Read::_avIOBufferRead(void* opaque, uint8_t* buf, int bufSize)
+ {
+ AVIOBufferData* bufferData = static_cast(opaque);
+
+ const int64_t remaining = bufferData->size - bufferData->offset;
+ int bufSizeClamped = std::min(std::max(
+ static_cast(bufSize),
+ static_cast(0)),
+ remaining);
+ if (!bufSizeClamped)
+ {
+ return AVERROR_EOF;
+ }
- return bufSizeClamped;
- }
+ memcpy(buf, bufferData->data + bufferData->offset, bufSizeClamped);
+ bufferData->offset += bufSizeClamped;
- int64_t FFmpegRead::_avIOBufferSeek(void* opaque, int64_t offset, int whence)
- {
- AVIOBufferData* bufferData = static_cast(opaque);
+ return bufSizeClamped;
+ }
- if (whence & AVSEEK_SIZE)
+ int64_t Read::_avIOBufferSeek(void* opaque, int64_t offset, int whence)
{
- return bufferData->size;
- }
+ AVIOBufferData* bufferData = static_cast(opaque);
+
+ if (whence & AVSEEK_SIZE)
+ {
+ return bufferData->size;
+ }
- bufferData->offset = std::min(std::max(
- offset,
- static_cast(0)),
- static_cast(bufferData->size));
+ bufferData->offset = std::min(std::max(
+ offset,
+ static_cast(0)),
+ static_cast(bufferData->size));
- return offset;
+ return offset;
+ }
}
}
diff --git a/lib/toucan/FFmpegRead.h b/lib/toucan/FFmpegRead.h
index c7b9896..549c792 100644
--- a/lib/toucan/FFmpegRead.h
+++ b/lib/toucan/FFmpegRead.h
@@ -3,9 +3,11 @@
#pragma once
-#include "MemoryMap.h"
+#include
-#include
+#include
+
+#include
#include
@@ -13,7 +15,6 @@ extern "C"
{
#include
#include
-#include
#include
} // extern "C"
@@ -22,55 +23,58 @@ extern "C"
namespace toucan
{
- class FFmpegRead : public std::enable_shared_from_this
+ namespace ffmpeg
{
- public:
- FFmpegRead(
- const std::filesystem::path&,
- const MemoryReference& = {});
-
- virtual ~FFmpegRead();
-
- const OIIO::ImageSpec& getSpec();
- const OTIO_NS::TimeRange& getTimeRange() const;
-
- OIIO::ImageBuf getImage(const OTIO_NS::RationalTime&);
-
- private:
- void _seek(const OTIO_NS::RationalTime&);
- OIIO::ImageBuf _read();
-
- std::filesystem::path _path;
- MemoryReference _memoryReference;
- OIIO::ImageSpec _spec;
- OTIO_NS::TimeRange _timeRange;
- OTIO_NS::RationalTime _currentTime;
-
- struct AVIOBufferData
+ class Read : public std::enable_shared_from_this
{
- AVIOBufferData();
- AVIOBufferData(const uint8_t*, size_t size);
-
- const uint8_t* data = nullptr;
- size_t size = 0;
- size_t offset = 0;
+ public:
+ Read(
+ const std::filesystem::path&,
+ const MemoryReference& = {});
+
+ virtual ~Read();
+
+ const OIIO::ImageSpec& getSpec();
+ const OTIO_NS::TimeRange& getTimeRange() const;
+
+ OIIO::ImageBuf getImage(const OTIO_NS::RationalTime&);
+
+ private:
+ void _seek(const OTIO_NS::RationalTime&);
+ OIIO::ImageBuf _read();
+
+ std::filesystem::path _path;
+ MemoryReference _memoryReference;
+ OIIO::ImageSpec _spec;
+ OTIO_NS::TimeRange _timeRange;
+ OTIO_NS::RationalTime _currentTime;
+
+ struct AVIOBufferData
+ {
+ AVIOBufferData();
+ AVIOBufferData(const uint8_t*, size_t size);
+
+ const uint8_t* data = nullptr;
+ size_t size = 0;
+ size_t offset = 0;
+ };
+ static int _avIOBufferRead(void* opaque, uint8_t* buf, int bufSize);
+ static int64_t _avIOBufferSeek(void* opaque, int64_t offset, int whence);
+
+ AVFormatContext* _avFormatContext = nullptr;
+ AVIOBufferData _avIOBufferData;
+ uint8_t* _avIOContextBuffer = nullptr;
+ AVIOContext* _avIOContext = nullptr;
+ AVRational _avSpeed = { 24, 1 };
+ int _avStream = -1;
+ std::map _avCodecParameters;
+ std::map _avCodecContext;
+ AVFrame* _avFrame = nullptr;
+ AVFrame* _avFrame2 = nullptr;
+ AVPixelFormat _avInputPixelFormat = AV_PIX_FMT_NONE;
+ AVPixelFormat _avOutputPixelFormat = AV_PIX_FMT_NONE;
+ SwsContext* _swsContext = nullptr;
+ bool _eof = false;
};
- static int _avIOBufferRead(void* opaque, uint8_t* buf, int bufSize);
- static int64_t _avIOBufferSeek(void* opaque, int64_t offset, int whence);
-
- AVFormatContext* _avFormatContext = nullptr;
- AVIOBufferData _avIOBufferData;
- uint8_t* _avIOContextBuffer = nullptr;
- AVIOContext* _avIOContext = nullptr;
- AVRational _avSpeed = { 24, 1 };
- int _avStream = -1;
- std::map _avCodecParameters;
- std::map _avCodecContext;
- AVFrame* _avFrame = nullptr;
- AVFrame* _avFrame2 = nullptr;
- AVPixelFormat _avInputPixelFormat = AV_PIX_FMT_NONE;
- AVPixelFormat _avOutputPixelFormat = AV_PIX_FMT_NONE;
- SwsContext* _swsContext = nullptr;
- bool _eof = false;
- };
+ }
}
diff --git a/lib/toucan/FFmpegWrite.cpp b/lib/toucan/FFmpegWrite.cpp
new file mode 100644
index 0000000..2fa9da9
--- /dev/null
+++ b/lib/toucan/FFmpegWrite.cpp
@@ -0,0 +1,305 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Contributors to the toucan project.
+
+#include "FFmpegWrite.h"
+
+#include "Util.h"
+
+#include
+#include
+
+extern "C"
+{
+#include
+#include
+}
+
+namespace toucan
+{
+ namespace ffmpeg
+ {
+ Write::Write(
+ const std::filesystem::path& path,
+ const OIIO::ImageSpec& spec,
+ const OTIO_NS::TimeRange& timeRange,
+ VideoCodec videoCodec) :
+ _path(path),
+ _spec(spec),
+ _timeRange(timeRange)
+ {
+ av_log_set_level(AV_LOG_QUIET);
+ //av_log_set_level(AV_LOG_VERBOSE);
+ //av_log_set_callback(log);
+
+ AVCodecID avCodecID = getVideoCodecId(videoCodec);
+ int avProfile = getVideoCodecProfile(videoCodec);
+
+ int r = avformat_alloc_output_context2(&_avFormatContext, NULL, NULL, _path.string().c_str());
+ if (r < 0)
+ {
+ throw std::runtime_error(getErrorLabel(r));
+ }
+ const AVCodec* avCodec = avcodec_find_encoder(avCodecID);
+ if (!avCodec)
+ {
+ throw std::runtime_error("Cannot find encoder");
+ }
+ _avCodecContext = avcodec_alloc_context3(avCodec);
+ if (!_avCodecContext)
+ {
+ throw std::runtime_error("Cannot allocate context");
+ }
+ _avVideoStream = avformat_new_stream(_avFormatContext, avCodec);
+ if (!_avVideoStream)
+ {
+ throw std::runtime_error("Cannot allocate stream");
+ }
+ if (!avCodec->pix_fmts)
+ {
+ throw std::runtime_error("No pixel formats available");
+ }
+
+ _avCodecContext->codec_id = avCodec->id;
+ _avCodecContext->codec_type = AVMEDIA_TYPE_VIDEO;
+ _avCodecContext->width = spec.width;
+ _avCodecContext->height = spec.height;
+ _avCodecContext->sample_aspect_ratio = AVRational({ 1, 1 });
+ _avCodecContext->pix_fmt = avCodec->pix_fmts[0];
+ const auto rational = toRational(timeRange.duration().rate());
+ _avCodecContext->time_base = { rational.second, rational.first };
+ _avCodecContext->framerate = { rational.first, rational.second };
+ _avCodecContext->profile = avProfile;
+ if (_avFormatContext->oformat->flags & AVFMT_GLOBALHEADER)
+ {
+ _avCodecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
+ }
+ _avCodecContext->thread_count = 0;
+ _avCodecContext->thread_type = FF_THREAD_FRAME;
+
+ r = avcodec_open2(_avCodecContext, avCodec, NULL);
+ if (r < 0)
+ {
+ throw std::runtime_error(getErrorLabel(r));
+ }
+
+ r = avcodec_parameters_from_context(_avVideoStream->codecpar, _avCodecContext);
+ if (r < 0)
+ {
+ throw std::runtime_error(getErrorLabel(r));
+ }
+
+ _avVideoStream->time_base = { rational.second, rational.first };
+ _avVideoStream->avg_frame_rate = { rational.first, rational.second };
+
+ //av_dump_format(_avFormatContext, 0, _path.string().c_str(), 1);
+
+ r = avio_open(&_avFormatContext->pb, _path.string().c_str(), AVIO_FLAG_WRITE);
+ if (r < 0)
+ {
+ throw std::runtime_error(getErrorLabel(r));
+ }
+
+ r = avformat_write_header(_avFormatContext, NULL);
+ if (r < 0)
+ {
+ throw std::runtime_error(getErrorLabel(r));
+ }
+
+ _avPacket = av_packet_alloc();
+ if (!_avPacket)
+ {
+ throw std::runtime_error("Cannot allocate packet");
+ }
+
+ _avFrame = av_frame_alloc();
+ if (!_avFrame)
+ {
+ throw std::runtime_error("Cannot allocate frame");
+ }
+ _avFrame->format = _avVideoStream->codecpar->format;
+ _avFrame->width = _avVideoStream->codecpar->width;
+ _avFrame->height = _avVideoStream->codecpar->height;
+ r = av_frame_get_buffer(_avFrame, 0);
+ if (r < 0)
+ {
+ throw std::runtime_error(getErrorLabel(r));
+ }
+
+ _avFrame2 = av_frame_alloc();
+ if (!_avFrame2)
+ {
+ throw std::runtime_error("Cannot allocate frame");
+ }
+
+ _opened = true;
+ }
+
+ Write::~Write()
+ {
+ if (_opened)
+ {
+ _encodeVideo(nullptr);
+ av_write_trailer(_avFormatContext);
+ }
+ if (_swsContext)
+ {
+ sws_freeContext(_swsContext);
+ }
+ if (_avFrame2)
+ {
+ av_frame_free(&_avFrame2);
+ }
+ if (_avFrame)
+ {
+ av_frame_free(&_avFrame);
+ }
+ if (_avPacket)
+ {
+ av_packet_free(&_avPacket);
+ }
+ if (_avCodecContext)
+ {
+ avcodec_free_context(&_avCodecContext);
+ }
+ if (_avFormatContext && _avFormatContext->pb)
+ {
+ avio_closep(&_avFormatContext->pb);
+ }
+ if (_avFormatContext)
+ {
+ avformat_free_context(_avFormatContext);
+ }
+ }
+
+ void Write::writeImage(const OIIO::ImageBuf& buf, const OTIO_NS::RationalTime& time)
+ {
+ const auto& spec = buf.spec();
+ AVPixelFormat avPixelFormatIn = AV_PIX_FMT_NONE;
+ switch (spec.nchannels)
+ {
+ case 1:
+ switch (spec.format.basetype)
+ {
+ case OIIO::TypeDesc::UINT8: avPixelFormatIn = AV_PIX_FMT_GRAY8; break;
+ case OIIO::TypeDesc::UINT16: avPixelFormatIn = AV_PIX_FMT_GRAY16; break;
+ default: break;
+ }
+ break;
+ case 3:
+ switch (spec.format.basetype)
+ {
+ case OIIO::TypeDesc::UINT8: avPixelFormatIn = AV_PIX_FMT_RGB24; break;
+ case OIIO::TypeDesc::UINT16: avPixelFormatIn = AV_PIX_FMT_RGB48; break;
+ default: break;
+ }
+ break;
+ case 4:
+ switch (spec.format.basetype)
+ {
+ case OIIO::TypeDesc::UINT8: avPixelFormatIn = AV_PIX_FMT_RGBA; break;
+ case OIIO::TypeDesc::UINT16: avPixelFormatIn = AV_PIX_FMT_RGBA64; break;
+ default: break;
+ }
+ break;
+ default: break;
+ }
+ if (AV_PIX_FMT_NONE == avPixelFormatIn)
+ {
+ throw std::runtime_error("Incompatible pixel type");
+ }
+ if (spec.width != _spec.width ||
+ spec.height != _spec.height ||
+ avPixelFormatIn != _avPixelFormatIn)
+ {
+ _avPixelFormatIn = avPixelFormatIn;
+ if (_swsContext)
+ {
+ sws_freeContext(_swsContext);
+ }
+ /*_swsContext = sws_getContext(
+ spec.width,
+ spec.height,
+ _avPixelFormatIn,
+ spec.width,
+ spec.height,
+ _avCodecContext->pix_fmt,
+ swsScaleFlags,
+ 0,
+ 0,
+ 0);*/
+ _swsContext = sws_alloc_context();
+ if (!_swsContext)
+ {
+ throw std::runtime_error("Cannot allocate context");
+ }
+ av_opt_set_defaults(_swsContext);
+ int r = av_opt_set_int(_swsContext, "srcw", spec.width, AV_OPT_SEARCH_CHILDREN);
+ r = av_opt_set_int(_swsContext, "srch", spec.height, AV_OPT_SEARCH_CHILDREN);
+ r = av_opt_set_int(_swsContext, "src_format", _avPixelFormatIn, AV_OPT_SEARCH_CHILDREN);
+ r = av_opt_set_int(_swsContext, "dstw", spec.width, AV_OPT_SEARCH_CHILDREN);
+ r = av_opt_set_int(_swsContext, "dsth", spec.height, AV_OPT_SEARCH_CHILDREN);
+ r = av_opt_set_int(_swsContext, "dst_format", _avCodecContext->pix_fmt, AV_OPT_SEARCH_CHILDREN);
+ r = av_opt_set_int(_swsContext, "sws_flags", SWS_BICUBIC, AV_OPT_SEARCH_CHILDREN);
+ r = av_opt_set_int(_swsContext, "threads", 0, AV_OPT_SEARCH_CHILDREN);
+ r = sws_init_context(_swsContext, nullptr, nullptr);
+ if (r < 0)
+ {
+ throw std::runtime_error(getErrorLabel(r));
+ }
+ }
+
+ av_image_fill_arrays(
+ _avFrame2->data,
+ _avFrame2->linesize,
+ reinterpret_cast(buf.localpixels()),
+ _avPixelFormatIn,
+ spec.width,
+ spec.height,
+ 1);
+
+ sws_scale(
+ _swsContext,
+ (uint8_t const* const*)_avFrame2->data,
+ _avFrame2->linesize,
+ 0,
+ _spec.height,
+ _avFrame->data,
+ _avFrame->linesize);
+
+ const auto timeRational = toRational(time.rate());
+ _avFrame->pts = av_rescale_q(
+ (time - _timeRange.start_time()).value(),
+ { timeRational.second, timeRational.first },
+ _avVideoStream->time_base);
+ _encodeVideo(_avFrame);
+ }
+
+ void Write::_encodeVideo(AVFrame* frame)
+ {
+ int r = avcodec_send_frame(_avCodecContext, frame);
+ if (r < 0)
+ {
+ throw std::runtime_error(getErrorLabel(r));
+ }
+
+ while (r >= 0)
+ {
+ r = avcodec_receive_packet(_avCodecContext, _avPacket);
+ if (r == AVERROR(EAGAIN) || r == AVERROR_EOF)
+ {
+ return;
+ }
+ else if (r < 0)
+ {
+ throw std::runtime_error(getErrorLabel(r));
+ }
+ r = av_interleaved_write_frame(_avFormatContext, _avPacket);
+ if (r < 0)
+ {
+ throw std::runtime_error(getErrorLabel(r));
+ }
+ av_packet_unref(_avPacket);
+ }
+ }
+ }
+}
diff --git a/lib/toucan/FFmpegWrite.h b/lib/toucan/FFmpegWrite.h
new file mode 100644
index 0000000..05d234d
--- /dev/null
+++ b/lib/toucan/FFmpegWrite.h
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Contributors to the toucan project.
+
+#pragma once
+
+#include
+
+#include
+
+#include
+
+extern "C"
+{
+#include
+#include
+
+} // extern "C"
+
+#include
+
+namespace toucan
+{
+ namespace ffmpeg
+ {
+ class Write : public std::enable_shared_from_this
+ {
+ public:
+ Write(
+ const std::filesystem::path&,
+ const OIIO::ImageSpec&,
+ const OTIO_NS::TimeRange&,
+ VideoCodec);
+
+ virtual ~Write();
+
+ void writeImage(const OIIO::ImageBuf&, const OTIO_NS::RationalTime&);
+
+ private:
+ void _encodeVideo(AVFrame*);
+
+ std::filesystem::path _path;
+ OIIO::ImageSpec _spec;
+ OTIO_NS::TimeRange _timeRange;
+ AVFormatContext* _avFormatContext = nullptr;
+ AVCodecContext* _avCodecContext = nullptr;
+ AVStream* _avVideoStream = nullptr;
+ AVPacket* _avPacket = nullptr;
+ AVFrame* _avFrame = nullptr;
+ AVPixelFormat _avPixelFormatIn = AV_PIX_FMT_NONE;
+ AVFrame* _avFrame2 = nullptr;
+ SwsContext* _swsContext = nullptr;
+ bool _opened = false;
+ };
+ }
+}
diff --git a/lib/toucan/Read.cpp b/lib/toucan/Read.cpp
index 2c55e78..440b73b 100644
--- a/lib/toucan/Read.cpp
+++ b/lib/toucan/Read.cpp
@@ -22,7 +22,7 @@ namespace toucan
// Open the file ane get information.
try
{
- _ffRead = std::make_unique(path, memoryReference);
+ _ffRead = std::make_unique(path, memoryReference);
_spec = _ffRead->getSpec();
_timeRange = _ffRead->getTimeRange();
}
diff --git a/lib/toucan/Read.h b/lib/toucan/Read.h
index f305277..b61ea98 100644
--- a/lib/toucan/Read.h
+++ b/lib/toucan/Read.h
@@ -35,7 +35,7 @@ namespace toucan
private:
std::filesystem::path _path;
- std::unique_ptr _ffRead;
+ std::unique_ptr _ffRead;
std::shared_ptr _memoryReader;
std::unique_ptr _input;
OIIO::ImageSpec _spec;
diff --git a/lib/toucan/Util.cpp b/lib/toucan/Util.cpp
index 5afd9e7..0fb062a 100644
--- a/lib/toucan/Util.cpp
+++ b/lib/toucan/Util.cpp
@@ -117,6 +117,36 @@ namespace toucan
return out;
}
+ std::string join(const std::vector& values, char delimeter)
+ {
+ std::string out;
+ const std::size_t size = values.size();
+ for (std::size_t i = 0; i < size; ++i)
+ {
+ out += values[i];
+ if (i < size - 1)
+ {
+ out += delimeter;
+ }
+ }
+ return out;
+ }
+
+ std::string join(const std::vector& values, const std::string& delimeter)
+ {
+ std::string out;
+ const std::size_t size = values.size();
+ for (std::size_t i = 0; i < size; ++i)
+ {
+ out += values[i];
+ if (i < size - 1)
+ {
+ out += delimeter;
+ }
+ }
+ return out;
+ }
+
std::string getSequenceFrame(
const std::filesystem::path& path,
const std::string& namePrefix,
diff --git a/lib/toucan/Util.h b/lib/toucan/Util.h
index b14ee03..db3d9af 100644
--- a/lib/toucan/Util.h
+++ b/lib/toucan/Util.h
@@ -34,6 +34,12 @@ namespace toucan
//! Split the URL protocol.
std::pair splitURLProtocol(const std::string&);
+ //! Join a list of strings.
+ std::string join(const std::vector&, char delimeter);
+
+ //! Join a list of strings.
+ std::string join(const std::vector&, const std::string& delimeter);
+
//! Get an image sequence file name.
std::string getSequenceFrame(
const std::filesystem::path&,
diff --git a/lib/toucanView/App.cpp b/lib/toucanView/App.cpp
index 05ffae9..b37f198 100644
--- a/lib/toucanView/App.cpp
+++ b/lib/toucanView/App.cpp
@@ -60,7 +60,7 @@ namespace toucan
_filesModel = std::make_shared(context, _host);
- _windowModel = std::make_shared();
+ _windowModel = std::make_shared(context);
_window = MainWindow::create(
context,
diff --git a/lib/toucanView/App.h b/lib/toucanView/App.h
index 61651cb..a73cd84 100644
--- a/lib/toucanView/App.h
+++ b/lib/toucanView/App.h
@@ -14,6 +14,7 @@ namespace toucan
class TimeUnitsModel;
class WindowModel;
+ //! Application.
class App : public dtk::App
{
protected:
@@ -24,13 +25,21 @@ namespace toucan
public:
virtual ~App();
+ //! Create a new application.
static std::shared_ptr create(
const std::shared_ptr&,
std::vector&);
+ //! Get the time units model.
const std::shared_ptr& getTimeUnitsModel() const;
+
+ //! Get the image effect host.
const std::shared_ptr& getHost() const;
+
+ //! Get the files model.
const std::shared_ptr& getFilesModel() const;
+
+ //! Get the window model.
const std::shared_ptr& getWindowModel() const;
private:
diff --git a/lib/toucanView/ClipItem.h b/lib/toucanView/ClipItem.h
index 4608f4f..35bc1b2 100644
--- a/lib/toucanView/ClipItem.h
+++ b/lib/toucanView/ClipItem.h
@@ -9,6 +9,7 @@
namespace toucan
{
+ //! Timeline clip item.
class ClipItem : public IItem
{
protected:
@@ -22,6 +23,7 @@ namespace toucan
public:
virtual ~ClipItem();
+ //! Create a new item.
static std::shared_ptr create(
const std::shared_ptr&,
const std::shared_ptr&,
diff --git a/lib/toucanView/ExportTool.cpp b/lib/toucanView/ExportTool.cpp
index 5d9450a..ab7fd02 100644
--- a/lib/toucanView/ExportTool.cpp
+++ b/lib/toucanView/ExportTool.cpp
@@ -9,9 +9,15 @@
#include
+#include
#include
-#include
+#include
#include
+#include
+
+#include
+
+#include
namespace toucan
{
@@ -23,60 +29,198 @@ namespace toucan
IWidget::_init(context, "toucan::ExportWidget", parent);
_host = app->getHost();
- _formats =
+ _movieCodecs = ffmpeg::getVideoCodecStrings();
+
+ std::string outputPath;
+ int currentTab = 0;
+ std::string imageBaseName = "render.";
+ int imagePadding = 0;
+ std::string imageExtension = ".tiff";
+ std::string movieBaseName = "render";
+ std::string movieExtension = ".mov";
+ std::string movieCodec = "MJPEG";
+ try
{
- ".exr",
- ".tiff",
- ".png"
- };
+ auto settings = context->getSystem();
+ const auto json = std::any_cast(settings->get("ExportWidget"));
+ auto i = json.find("OutputPath");
+ if (i != json.end() && i->is_string())
+ {
+ outputPath = i->get();
+ }
+ i = json.find("CurrentTab");
+ if (i != json.end() && i->is_number())
+ {
+ currentTab = i->get();
+ }
+ i = json.find("ImageBaseName");
+ if (i != json.end() && i->is_string())
+ {
+ imageBaseName = i->get();
+ }
+ i = json.find("ImagePadding");
+ if (i != json.end() && i->is_number())
+ {
+ imagePadding = i->get();
+ }
+ i = json.find("ImageExtension");
+ if (i != json.end() && i->is_string())
+ {
+ imageExtension = i->get();
+ }
+ i = json.find("MovieBaseName");
+ if (i != json.end() && i->is_string())
+ {
+ movieBaseName = i->get();
+ }
+ i = json.find("MovieExtension");
+ if (i != json.end() && i->is_string())
+ {
+ movieExtension = i->get();
+ }
+ i = json.find("MovieCodec");
+ if (i != json.end() && i->is_string())
+ {
+ movieCodec = i->get();
+ }
+ }
+ catch (const std::exception&)
+ {}
_layout = dtk::VerticalLayout::create(context, shared_from_this());
- _layout->setMarginRole(dtk::SizeRole::MarginSmall);
- _layout->setSpacingRole(dtk::SizeRole::SpacingSmall);
+ _layout->setSpacingRole(dtk::SizeRole::None);
+
+ _outputLayout = dtk::VerticalLayout::create(context, _layout);
+ _outputLayout->setMarginRole(dtk::SizeRole::Margin);
+ auto label = dtk::Label::create(context, "Output directory:", _outputLayout);
+ _outputPathEdit = dtk::FileEdit::create(context, _outputLayout);
+ _outputPathEdit->setPath(outputPath);
+
+ auto divider = dtk::Divider::create(context, dtk::Orientation::Vertical, _layout);
- auto vLayout = dtk::VerticalLayout::create(context, _layout);
- vLayout->setSpacingRole(dtk::SizeRole::SpacingSmall);
- auto label = dtk::Label::create(context, "Output directory:", vLayout);
- _outputPathEdit = dtk::FileEdit::create(context, vLayout);
+ _tabWidget = dtk::TabWidget::create(context, _layout);
- auto gridLayout = dtk::GridLayout::create(context, _layout);
- gridLayout->setSpacingRole(dtk::SizeRole::SpacingSmall);
+ _imageLayout = dtk::VerticalLayout::create(context);
+ _imageLayout->setMarginRole(dtk::SizeRole::Margin);
+ _tabWidget->addTab("Images", _imageLayout);
+
+ auto gridLayout = dtk::GridLayout::create(context, _imageLayout);
label = dtk::Label::create(context, "Base name:", gridLayout);
gridLayout->setGridPos(label, 0, 0);
- _baseNameEdit = dtk::LineEdit::create(context, gridLayout);
- _baseNameEdit->setText("render.");
- gridLayout->setGridPos(_baseNameEdit, 0, 1);
+ _imageBaseNameEdit = dtk::LineEdit::create(context, gridLayout);
+ _imageBaseNameEdit->setText(imageBaseName);
+ gridLayout->setGridPos(_imageBaseNameEdit, 0, 1);
label = dtk::Label::create(context, "Number padding:", gridLayout);
gridLayout->setGridPos(label, 1, 0);
- _paddingEdit = dtk::IntEdit::create(context, gridLayout);
- _paddingEdit->setRange(dtk::RangeI(0, 9));
- gridLayout->setGridPos(_paddingEdit, 1, 1);
+ _imagePaddingEdit = dtk::IntEdit::create(context, gridLayout);
+ _imagePaddingEdit->setRange(dtk::RangeI(0, 9));
+ _imagePaddingEdit->setValue(imagePadding);
+ gridLayout->setGridPos(_imagePaddingEdit, 1, 1);
- label = dtk::Label::create(context, "File format:", gridLayout);
+ label = dtk::Label::create(context, "Extension:", gridLayout);
gridLayout->setGridPos(label, 2, 0);
- _formatComboBox = dtk::ComboBox::create(context, _formats, gridLayout);
- gridLayout->setGridPos(_formatComboBox, 2, 1);
+ _imageExtensionEdit = dtk::LineEdit::create(context, gridLayout);
+ _imageExtensionEdit->setText(imageExtension);
+ gridLayout->setGridPos(_imageExtensionEdit, 2, 1);
- auto divider = dtk::Divider::create(context, dtk::Orientation::Vertical, _layout);
+ label = dtk::Label::create(context, "Filename:", gridLayout);
+ gridLayout->setGridPos(label, 3, 0);
+ _imageFilenameLabel = dtk::Label::create(context, gridLayout);
+ gridLayout->setGridPos(_imageFilenameLabel, 3, 1);
- auto exportSequenceButton = dtk::PushButton::create(
+ divider = dtk::Divider::create(context, dtk::Orientation::Vertical, _imageLayout);
+
+ _exportSequenceButton = dtk::PushButton::create(
context,
"Export Sequence",
- _layout);
- exportSequenceButton->setClickedCallback(
+ _imageLayout);
+
+ _exportStillButton = dtk::PushButton::create(
+ context,
+ "Export Still Frame",
+ _imageLayout);
+
+ _movieLayout = dtk::VerticalLayout::create(context);
+ _movieLayout->setMarginRole(dtk::SizeRole::Margin);
+ _tabWidget->addTab("Movie", _movieLayout);
+ _tabWidget->setCurrentTab(currentTab);
+
+ gridLayout = dtk::GridLayout::create(context, _movieLayout);
+
+ label = dtk::Label::create(context, "Base name:", gridLayout);
+ gridLayout->setGridPos(label, 0, 0);
+ _movieBaseNameEdit = dtk::LineEdit::create(context, gridLayout);
+ _movieBaseNameEdit->setText(movieBaseName);
+ gridLayout->setGridPos(_movieBaseNameEdit, 0, 1);
+
+ label = dtk::Label::create(context, "Extension:", gridLayout);
+ gridLayout->setGridPos(label, 1, 0);
+ _movieExtensionEdit = dtk::LineEdit::create(context, gridLayout);
+ _movieExtensionEdit->setText(movieExtension);
+ gridLayout->setGridPos(_movieExtensionEdit, 1, 1);
+
+ label = dtk::Label::create(context, "Filename:", gridLayout);
+ gridLayout->setGridPos(label, 2, 0);
+ _movieFilenameLabel = dtk::Label::create(context, gridLayout);
+ gridLayout->setGridPos(_movieFilenameLabel, 2, 1);
+
+ label = dtk::Label::create(context, "Codec:", gridLayout);
+ gridLayout->setGridPos(label, 3, 0);
+ _movieCodecComboBox = dtk::ComboBox::create(context, _movieCodecs, gridLayout);
+ ffmpeg::VideoCodec ffmpegVideoCodec = ffmpeg::VideoCodec::First;
+ ffmpeg::fromString(movieCodec, ffmpegVideoCodec);
+ _movieCodecComboBox->setCurrentIndex(static_cast(ffmpegVideoCodec));
+ gridLayout->setGridPos(_movieCodecComboBox, 3, 1);
+
+ divider = dtk::Divider::create(context, dtk::Orientation::Vertical, _movieLayout);
+
+ _exportMovieButton = dtk::PushButton::create(
+ context,
+ "Export Movie",
+ _movieLayout);
+
+ _widgetUpdate();
+
+ _imageBaseNameEdit->setTextChangedCallback(
+ [this](const std::string&)
+ {
+ _widgetUpdate();
+ });
+
+ _imagePaddingEdit->setCallback(
+ [this](int)
+ {
+ _widgetUpdate();
+ });
+
+ _imageExtensionEdit->setTextChangedCallback(
+ [this](const std::string&)
+ {
+ _widgetUpdate();
+ });
+
+ _movieBaseNameEdit->setTextChangedCallback(
+ [this](const std::string&)
+ {
+ _widgetUpdate();
+ });
+
+ _movieExtensionEdit->setTextChangedCallback(
+ [this](const std::string&)
+ {
+ _widgetUpdate();
+ });
+
+ _exportSequenceButton->setClickedCallback(
[this]
{
_timeRange = _file->getPlaybackModel()->getInOutRange();
_export();
});
- auto exportCurrentButton = dtk::PushButton::create(
- context,
- "Export Current Frame",
- _layout);
- exportCurrentButton->setClickedCallback(
+ _exportStillButton->setClickedCallback(
[this]
{
_timeRange = OTIO_NS::TimeRange(
@@ -85,6 +229,32 @@ namespace toucan
_export();
});
+ _exportMovieButton->setClickedCallback(
+ [this]
+ {
+ _timeRange = _file->getPlaybackModel()->getInOutRange();
+ const std::string baseName = _movieBaseNameEdit->getText();
+ const std::string extension = _movieExtensionEdit->getText();
+ const std::filesystem::path path = _outputPathEdit->getPath() / (baseName + extension);
+ const IMATH_NAMESPACE::V2d imageSize = _graph->getImageSize();
+ ffmpeg::VideoCodec videoCodec = ffmpeg::VideoCodec::First;
+ ffmpeg::fromString(_movieCodecs[_movieCodecComboBox->getCurrentIndex()], videoCodec);
+ try
+ {
+ _ffWrite = std::make_shared(
+ path,
+ OIIO::ImageSpec(imageSize.x, imageSize.y, 3),
+ _timeRange,
+ videoCodec);
+ _export();
+ }
+ catch (const std::exception& e)
+ {
+ auto dialogSystem = getContext()->getSystem();
+ dialogSystem->message("ERROR", e.what(), getWindow());
+ }
+ });
+
_timer = dtk::Timer::create(context);
_timer->setRepeating(true);
@@ -105,12 +275,28 @@ namespace toucan
_time = OTIO_NS::RationalTime();
_graph.reset();
}
- setEnabled(_file.get());
+ _exportSequenceButton->setEnabled(_file.get());
+ _exportStillButton->setEnabled(_file.get());
+ _exportMovieButton->setEnabled(_file.get());
});
}
ExportWidget::~ExportWidget()
- {}
+ {
+ nlohmann::json json;
+ json["OutputPath"] = _outputPathEdit->getPath().string();
+ json["CurrentTab"] = _tabWidget->getCurrentTab();
+ json["ImageBaseName"] = _imageBaseNameEdit->getText();
+ json["ImagePadding"] = _imagePaddingEdit->getValue();
+ json["ImageExtension"] = _imageExtensionEdit->getText();
+ json["MovieBaseName"] = _movieBaseNameEdit->getText();
+ json["MovieExtension"] = _movieExtensionEdit->getText();
+ json["MovieCodec"] = ffmpeg::toString(
+ static_cast(_movieCodecComboBox->getCurrentIndex()));
+ auto context = getContext();
+ auto settings = context->getSystem();
+ settings->set("ExportWidget", json);
+ }
std::shared_ptr ExportWidget::create(
const std::shared_ptr& context,
@@ -147,6 +333,7 @@ namespace toucan
[this]
{
_timer->stop();
+ _ffWrite.reset();
_dialog.reset();
});
_dialog->show();
@@ -158,32 +345,68 @@ namespace toucan
if (auto node = _graph->exec(_host, _time))
{
const auto buf = node->exec();
- const std::string fileName = getSequenceFrame(
- _outputPathEdit->getPath().string(),
- _baseNameEdit->getText(),
- _time.to_frames(),
- _paddingEdit->getValue(),
- _formats[_formatComboBox->getCurrentIndex()]);
- buf.write(fileName);
+ if (_ffWrite)
+ {
+ try
+ {
+ _ffWrite->writeImage(buf, _time);
+ }
+ catch (const std::exception& e)
+ {
+ if (_dialog)
+ {
+ _dialog->close();
+ }
+ auto dialogSystem = getContext()->getSystem();
+ dialogSystem->message("ERROR", e.what(), getWindow());
+ }
+ }
+ else
+ {
+ const std::string fileName = getSequenceFrame(
+ _outputPathEdit->getPath().string(),
+ _imageBaseNameEdit->getText(),
+ _time.to_frames(),
+ _imagePaddingEdit->getValue(),
+ _imageExtensionEdit->getText());
+ buf.write(fileName);
+ }
}
- const OTIO_NS::RationalTime end = _timeRange.end_time_inclusive();
- if (_time < end)
- {
- _time += OTIO_NS::RationalTime(1.0, _timeRange.duration().rate());
- const OTIO_NS::RationalTime duration = _timeRange.duration();
- const double v = duration.value() > 0.0 ?
- (_time - _timeRange.start_time()).value() / static_cast(duration.value()) :
- 0.0;
- _dialog->setValue(v);
- }
- else
+ if (_dialog)
{
- _dialog->close();
+ const OTIO_NS::RationalTime end = _timeRange.end_time_inclusive();
+ if (_time < end)
+ {
+ _time += OTIO_NS::RationalTime(1.0, _timeRange.duration().rate());
+ const OTIO_NS::RationalTime duration = _timeRange.duration();
+ const double v = duration.value() > 0.0 ?
+ (_time - _timeRange.start_time()).value() / static_cast(duration.value()) :
+ 0.0;
+ _dialog->setValue(v);
+ }
+ else
+ {
+ _dialog->close();
+ }
}
});
}
+ void ExportWidget::_widgetUpdate()
+ {
+ _imageFilenameLabel->setText(getSequenceFrame(
+ _outputPathEdit->getPath().string(),
+ _imageBaseNameEdit->getText(),
+ 0,
+ _imagePaddingEdit->getValue(),
+ _imageExtensionEdit->getText()));
+
+ _movieFilenameLabel->setText(
+ _movieBaseNameEdit->getText() +
+ _movieExtensionEdit->getText());
+ }
+
void ExportTool::_init(
const std::shared_ptr& context,
const std::shared_ptr& app,
diff --git a/lib/toucanView/ExportTool.h b/lib/toucanView/ExportTool.h
index 3049b87..1d5dd08 100644
--- a/lib/toucanView/ExportTool.h
+++ b/lib/toucanView/ExportTool.h
@@ -5,6 +5,7 @@
#include "IToolWidget.h"
+#include
#include
#include
@@ -14,14 +15,17 @@
#include
#include
#include
+#include
#include
#include
+#include
#include
namespace toucan
{
class File;
+ //! Export widget.
class ExportWidget : public dtk::IWidget
{
protected:
@@ -33,6 +37,7 @@ namespace toucan
public:
virtual ~ExportWidget();
+ //! Create a new widget.
static std::shared_ptr create(
const std::shared_ptr&,
const std::shared_ptr&,
@@ -43,25 +48,41 @@ namespace toucan
private:
void _export();
+ void _widgetUpdate();
std::shared_ptr _host;
std::shared_ptr _file;
OTIO_NS::TimeRange _timeRange;
OTIO_NS::RationalTime _time;
std::shared_ptr _graph;
- std::vector _formats;
+ std::vector _movieCodecs;
+ std::shared_ptr _ffWrite;
std::shared_ptr _layout;
+ std::shared_ptr _outputLayout;
std::shared_ptr _outputPathEdit;
- std::shared_ptr _baseNameEdit;
- std::shared_ptr _paddingEdit;
- std::shared_ptr _formatComboBox;
+ std::shared_ptr _tabWidget;
+ std::shared_ptr _imageLayout;
+ std::shared_ptr _imageBaseNameEdit;
+ std::shared_ptr _imagePaddingEdit;
+ std::shared_ptr _imageExtensionEdit;
+ std::shared_ptr _imageFilenameLabel;
+ std::shared_ptr _exportSequenceButton;
+ std::shared_ptr _exportStillButton;
+ std::shared_ptr _movieLayout;
+ std::shared_ptr _movieBaseNameEdit;
+ std::shared_ptr _movieExtensionEdit;
+ std::shared_ptr _movieCodecComboBox;
+ std::shared_ptr _movieFilenameLabel;
+ std::shared_ptr _exportMovieButton;
std::shared_ptr _dialog;
+
std::shared_ptr _timer;
std::shared_ptr > > _fileObserver;
};
+ //! Export tool.
class ExportTool : public IToolWidget
{
protected:
@@ -73,6 +94,7 @@ namespace toucan
public:
virtual ~ExportTool();
+ //! Create a new tool.
static std::shared_ptr create(
const std::shared_ptr&,
const std::shared_ptr&,
diff --git a/lib/toucanView/File.h b/lib/toucanView/File.h
index 6d84360..509da4a 100644
--- a/lib/toucanView/File.h
+++ b/lib/toucanView/File.h
@@ -20,6 +20,7 @@ namespace toucan
class ThumbnailGenerator;
class ViewModel;
+ //! Timeline file.
class File : std::enable_shared_from_this
{
public:
@@ -30,24 +31,46 @@ namespace toucan
~File();
+ //! Get the path.
const std::filesystem::path& getPath() const;
+ //! Get the timeline wrapper.
const std::shared_ptr& getTimelineWrapper() const;
+
+ //! Get the timeline.
const OTIO_NS::SerializableObject::Retainer& getTimeline() const;
+ //! Get the playback model.
const std::shared_ptr& getPlaybackModel() const;
+
+ //! Get the view model.
const std::shared_ptr& getViewModel() const;
+
+ //! Get the selection model.
const std::shared_ptr& getSelectionModel() const;
+
+ //! Get the thumbnail generator.
const std::shared_ptr& getThumbnailGenerator() const;
+ //! Get the image size.
const IMATH_NAMESPACE::V2i& getImageSize() const;
+
+ //! Get the number of image channels.
int getImageChannels() const;
+
+ //! Get the image data type.
const std::string& getImageDataType() const;
+ //! Observe the current image.
std::shared_ptr > > observeCurrentImage() const;
+ //! Observe the root node.
std::shared_ptr > > observeRootNode() const;
+
+ //! Observe the current node.
std::shared_ptr > > observeCurrentNode() const;
+
+ //! Set the current node.
void setCurrentNode(const std::shared_ptr&);
private:
diff --git a/lib/toucanView/FileTab.h b/lib/toucanView/FileTab.h
index ae83518..cd4f62f 100644
--- a/lib/toucanView/FileTab.h
+++ b/lib/toucanView/FileTab.h
@@ -11,7 +11,7 @@ namespace toucan
class File;
class Viewport;
- //! File tab.
+ //! Timeline file tab.
class FileTab : public dtk::IWidget
{
protected:
@@ -24,7 +24,7 @@ namespace toucan
public:
virtual ~FileTab();
- //! Create a new file tab.
+ //! Create a new tab.
static std::shared_ptr create(
const std::shared_ptr&,
const std::shared_ptr&,
diff --git a/lib/toucanView/FilesModel.h b/lib/toucanView/FilesModel.h
index 2d88bec..8f14017 100644
--- a/lib/toucanView/FilesModel.h
+++ b/lib/toucanView/FilesModel.h
@@ -15,7 +15,7 @@ namespace toucan
class File;
class ImageEffectHost;
- //! Files model.
+ //! Timeline files model.
class FilesModel : public std::enable_shared_from_this
{
public:
diff --git a/lib/toucanView/GapItem.cpp b/lib/toucanView/GapItem.cpp
index 6f98057..6608ddf 100644
--- a/lib/toucanView/GapItem.cpp
+++ b/lib/toucanView/GapItem.cpp
@@ -59,7 +59,7 @@ namespace toucan
_size.displayScale = event.displayScale;
_size.margin = event.style->getSizeRole(dtk::SizeRole::MarginInside, event.displayScale);
_size.border = event.style->getSizeRole(dtk::SizeRole::Border, event.displayScale);
- _size.fontInfo = event.style->getFontRole(dtk::FontRole::Label , event.displayScale);
+ _size.fontInfo = event.style->getFontRole(dtk::FontRole::Label, event.displayScale);
_size.fontMetrics = event.fontSystem->getMetrics(_size.fontInfo);
_size.textSize = event.fontSystem->getSize(_text, _size.fontInfo);
_draw.glyphs.clear();
diff --git a/lib/toucanView/GapItem.h b/lib/toucanView/GapItem.h
index 9beb6de..2243a7b 100644
--- a/lib/toucanView/GapItem.h
+++ b/lib/toucanView/GapItem.h
@@ -9,6 +9,7 @@
namespace toucan
{
+ //! Timeline gap item.
class GapItem : public IItem
{
protected:
@@ -21,6 +22,7 @@ namespace toucan
public:
virtual ~GapItem();
+ //! Create a new item.
static std::shared_ptr