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 ====== 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 create( const std::shared_ptr&, const std::shared_ptr&, diff --git a/lib/toucanView/GraphTool.h b/lib/toucanView/GraphTool.h index 780aa6f..8f4cfa3 100644 --- a/lib/toucanView/GraphTool.h +++ b/lib/toucanView/GraphTool.h @@ -16,6 +16,7 @@ namespace toucan { class File; + //! Image graph widget. class GraphWidget : public dtk::IWidget { protected: @@ -27,6 +28,7 @@ namespace toucan public: virtual ~GraphWidget(); + //! Create a new widget. static std::shared_ptr create( const std::shared_ptr&, const std::shared_ptr&, @@ -77,6 +79,7 @@ namespace toucan std::shared_ptr > > _currentNodeObserver; }; + //! Image graph tool. class GraphTool : public IToolWidget { protected: @@ -88,6 +91,7 @@ namespace toucan public: virtual ~GraphTool(); + //! Create a new tool. static std::shared_ptr create( const std::shared_ptr&, const std::shared_ptr&, diff --git a/lib/toucanView/IItem.h b/lib/toucanView/IItem.h index a21ef70..a88a02b 100644 --- a/lib/toucanView/IItem.h +++ b/lib/toucanView/IItem.h @@ -11,6 +11,7 @@ namespace toucan { class App; + //! Base class for timeline items. class IItem : public dtk::IWidget { protected: @@ -25,16 +26,25 @@ namespace toucan public: virtual ~IItem() = 0; + //! Get the OTIO item. const OTIO_NS::SerializableObject::Retainer& getItem() const; + //! Get the time range. const OTIO_NS::TimeRange& getTimeRange(); + //! Set the item scale. void setScale(double); + //! Get whether the item is selected. bool isSelected() const; + + //! Set whether the item is selected. void setSelected(bool); + //! Convert a position to a time. OTIO_NS::RationalTime posToTime(double) const; + + //! Convert a time to a position. int timeToPos(const OTIO_NS::RationalTime&) const; protected: diff --git a/lib/toucanView/IToolWidget.h b/lib/toucanView/IToolWidget.h index baab5a6..19e4c93 100644 --- a/lib/toucanView/IToolWidget.h +++ b/lib/toucanView/IToolWidget.h @@ -9,6 +9,7 @@ namespace toucan { class App; + //! Base class for tools. class IToolWidget : public dtk::IWidget { protected: @@ -22,6 +23,7 @@ namespace toucan public: virtual ~IToolWidget() = 0; + //! Get the tool text. const std::string& getText() const; protected: diff --git a/lib/toucanView/InfoBar.h b/lib/toucanView/InfoBar.h index f2a8ed2..dd14acc 100644 --- a/lib/toucanView/InfoBar.h +++ b/lib/toucanView/InfoBar.h @@ -14,6 +14,7 @@ namespace toucan class App; class File; + //! Information tool bar. class InfoBar : public dtk::IWidget { protected: @@ -25,6 +26,7 @@ namespace toucan public: virtual ~InfoBar(); + //! Create a new tool bar. static std::shared_ptr create( const std::shared_ptr&, const std::shared_ptr&, diff --git a/lib/toucanView/JSONTool.cpp b/lib/toucanView/JSONTool.cpp index e87568c..79a17d6 100644 --- a/lib/toucanView/JSONTool.cpp +++ b/lib/toucanView/JSONTool.cpp @@ -26,6 +26,7 @@ namespace toucan _text = dtk::split(item->to_json_string(), { '\n' }); _label = dtk::Label::create(context); + _label->setFontRole(dtk::FontRole::Mono); _label->setMarginRole(dtk::SizeRole::MarginSmall); _bellows = dtk::Bellows::create(context, item->name(), shared_from_this()); @@ -111,9 +112,6 @@ namespace toucan _scrollLayout->setSpacingRole(dtk::SizeRole::None); _scrollWidget->setWidget(_scrollLayout); - _nothingSelectedLabel = dtk::Label::create(context, "Nothing selected", _scrollLayout); - _nothingSelectedLabel->setMarginRole(dtk::SizeRole::MarginSmall); - dtk::Divider::create(context, dtk::Orientation::Vertical, _layout); _bottomLayout = dtk::HorizontalLayout::create(context, _layout); @@ -182,7 +180,6 @@ namespace toucan widget->setFilter(_searchBox->getText()); _widgets.push_back(widget); } - _nothingSelectedLabel->setVisible(selection.empty()); }); } else diff --git a/lib/toucanView/JSONTool.h b/lib/toucanView/JSONTool.h index 18ef45d..e341184 100644 --- a/lib/toucanView/JSONTool.h +++ b/lib/toucanView/JSONTool.h @@ -18,6 +18,7 @@ namespace toucan { class File; + //! JSON widget. class JSONWidget : public dtk::IWidget { protected: @@ -29,12 +30,16 @@ namespace toucan public: virtual ~JSONWidget(); + //! Create a new widget. static std::shared_ptr create( const std::shared_ptr&, const OTIO_NS::SerializableObject::Retainer&, const std::shared_ptr& parent = nullptr); + //! Set whether the widget is open. void setOpen(bool); + + //! Set the filter. void setFilter(const std::string&); void setGeometry(const dtk::Box2I&) override; @@ -50,6 +55,7 @@ namespace toucan std::shared_ptr _bellows; }; + //! JSON tool. class JSONTool : public IToolWidget { protected: @@ -61,6 +67,7 @@ namespace toucan public: virtual ~JSONTool(); + //! Create a new tool. static std::shared_ptr create( const std::shared_ptr&, const std::shared_ptr&, @@ -77,7 +84,6 @@ namespace toucan std::shared_ptr _scrollWidget; std::shared_ptr _scrollLayout; std::vector > _widgets; - std::shared_ptr _nothingSelectedLabel; std::shared_ptr _bottomLayout; std::shared_ptr > > _fileObserver; diff --git a/lib/toucanView/MainWindow.cpp b/lib/toucanView/MainWindow.cpp index 3587ab2..c47113f 100644 --- a/lib/toucanView/MainWindow.cpp +++ b/lib/toucanView/MainWindow.cpp @@ -17,8 +17,11 @@ #include #include +#include #include +#include + namespace toucan { void MainWindow::_init( @@ -31,6 +34,21 @@ namespace toucan _app = app; + float displayScale = 0.F; + try + { + auto settings = context->getSystem(); + const auto json = std::any_cast(settings->get("MainWindow")); + auto i = json.find("DisplayScale"); + if (i != json.end() && i->is_number()) + { + displayScale = i->get(); + } + } + catch (const std::exception&) + {} + setDisplayScale(displayScale); + _layout = dtk::VerticalLayout::create(context, shared_from_this()); _layout->setSpacingRole(dtk::SizeRole::None); @@ -73,14 +91,14 @@ namespace toucan _playbackBar = PlaybackBar::create(context, app, _bottomLayout); - _timelineDivider = dtk::Divider::create(context, dtk::Orientation::Vertical, _bottomLayout); + auto divider = dtk::Divider::create(context, dtk::Orientation::Vertical, _bottomLayout); _timelineWidget = TimelineWidget::create(context, app, _bottomLayout); _timelineWidget->setVStretch(dtk::Stretch::Expanding); - _infoDivider = dtk::Divider::create(context, dtk::Orientation::Vertical, _bottomLayout); + divider = dtk::Divider::create(context, dtk::Orientation::Vertical, _layout); - _infoBar = InfoBar::create(context, app, _bottomLayout); + _infoBar = InfoBar::create(context, app, _layout); std::weak_ptr appWeak(app); _tabWidget->setCallback( @@ -148,26 +166,19 @@ namespace toucan _tabWidget->setCurrentTab(index); }); - _controlsObserver = dtk::MapObserver::create( - app->getWindowModel()->observeControls(), - [this](const std::map& value) + _componentsObserver = dtk::MapObserver::create( + app->getWindowModel()->observeComponents(), + [this](const std::map& value) { - auto i = value.find(WindowControl::ToolBar); + auto i = value.find(WindowComponent::ToolBar); _toolBar->setVisible(i->second); _toolBarDivider->setVisible(i->second); - i = value.find(WindowControl::PlaybackBar); - _playbackBar->setVisible(i->second); - auto j = value.find(WindowControl::TimelineWidget); - _timelineWidget->setVisible(j->second); - auto k = value.find(WindowControl::InfoBar); - _infoBar->setVisible(k->second); - _bottomLayout->setVisible(i->second || j->second || k->second); - _timelineDivider->setVisible(i->second && j->second); - _infoDivider->setVisible((i->second || j->second) && k->second); - - i = value.find(WindowControl::Tools); + i = value.find(WindowComponent::ToolsPanel); _toolWidget->setVisible(i->second); + + i = value.find(WindowComponent::PlaybackPanel); + _bottomLayout->setVisible(i->second); }); _tooltipsObserver = dtk::ValueObserver::create( @@ -179,7 +190,13 @@ namespace toucan } MainWindow::~MainWindow() - {} + { + nlohmann::json json; + json["DisplayScale"] = getDisplayScale(); + auto context = getContext(); + auto settings = context->getSystem(); + settings->set("MainWindow", json); + } std::shared_ptr MainWindow::create( const std::shared_ptr& context, diff --git a/lib/toucanView/MainWindow.h b/lib/toucanView/MainWindow.h index 1771925..550cb12 100644 --- a/lib/toucanView/MainWindow.h +++ b/lib/toucanView/MainWindow.h @@ -24,6 +24,7 @@ namespace toucan class TimelineWidget; class ToolBar; + //! Main window. class MainWindow : public dtk::Window { protected: @@ -36,6 +37,7 @@ namespace toucan public: virtual ~MainWindow(); + //! Create a new window. static std::shared_ptr create( const std::shared_ptr&, const std::shared_ptr&, @@ -67,15 +69,13 @@ namespace toucan std::shared_ptr _bottomLayout; std::shared_ptr _playbackBar; std::shared_ptr _timelineWidget; - std::shared_ptr _timelineDivider; std::shared_ptr _infoBar; - std::shared_ptr _infoDivider; std::shared_ptr > > _filesObserver; std::shared_ptr > _addObserver; std::shared_ptr > _removeObserver; std::shared_ptr > _fileObserver; - std::shared_ptr > _controlsObserver; + std::shared_ptr > _componentsObserver; std::shared_ptr > _tooltipsObserver; }; } diff --git a/lib/toucanView/MenuBar.cpp b/lib/toucanView/MenuBar.cpp index 7f3f615..1f2bee4 100644 --- a/lib/toucanView/MenuBar.cpp +++ b/lib/toucanView/MenuBar.cpp @@ -35,8 +35,8 @@ namespace toucan _selectMenuInit(context, app); _timeMenuInit(context, app); _playbackMenuInit(context, app); - _windowMenuInit(context, app, window); _viewMenuInit(context, app); + _windowMenuInit(context, app, window); _filesObserver = dtk::ListObserver >::create( _filesModel->observeFiles(), @@ -67,8 +67,8 @@ namespace toucan _selectMenuUpdate(); _timeMenuUpdate(); _playbackMenuUpdate(); - _windowMenuUpdate(); _viewMenuUpdate(); + _windowMenuUpdate(); }); _fileIndexObserver = dtk::ValueObserver::create( @@ -570,6 +570,76 @@ namespace toucan _menus["Playback"]->addItem(_actions["Playback/Toggle"]); } + void MenuBar::_viewMenuInit( + const std::shared_ptr& context, + const std::shared_ptr& app) + { + _menus["View"] = dtk::Menu::create(context); + addMenu("View", _menus["View"]); + + _actions["View/ZoomIn"] = std::make_shared( + "Zoom In", + "ViewZoomIn", + dtk::Key::Equal, + 0, + [this] + { + if (_file) + { + _file->getViewModel()->zoomIn(); + } + }); + _actions["View/ZoomIn"]->toolTip = "View zoom in"; + _menus["View"]->addItem(_actions["View/ZoomIn"]); + + _actions["View/ZoomOut"] = std::make_shared( + "Zoom Out", + "ViewZoomOut", + dtk::Key::Minus, + 0, + [this] + { + if (_file) + { + _file->getViewModel()->zoomOut(); + } + }); + _actions["View/ZoomOut"]->toolTip = "View zoom out"; + _menus["View"]->addItem(_actions["View/ZoomOut"]); + + _actions["View/ZoomReset"] = std::make_shared( + "Zoom Reset", + "ViewZoomReset", + dtk::Key::_0, + 0, + [this] + { + if (_file) + { + _file->getViewModel()->zoomReset(); + } + }); + _actions["View/ZoomReset"]->toolTip = "Reset the view zoom"; + _menus["View"]->addItem(_actions["View/ZoomReset"]); + + _menus["View"]->addDivider(); + + _actions["View/Frame"] = std::make_shared( + "Frame View", + "ViewFrame", + dtk::Key::Backspace, + 0, + [this](bool value) + { + if (_file) + { + _file->getViewModel()->setFrameView(value); + } + }); + _actions["View/Frame"]->toolTip = "Frame the view"; + _menus["View"]->addItem(_actions["View/Frame"]); + } + void MenuBar::_windowMenuInit( const std::shared_ptr& context, const std::shared_ptr& app, @@ -596,33 +666,35 @@ namespace toucan _menus["Window"]->addDivider(); - struct Control + struct Component { - WindowControl control; + WindowComponent component; std::string action; std::string text; + std::string icon; + std::string tooltip; }; - const std::vector controls = + const std::vector components = { - { WindowControl::ToolBar, "ToolBar", "Tool Bar" }, - { WindowControl::PlaybackBar, "PlaybackBar", "Playback Bar" }, - { WindowControl::TimelineWidget, "TimelineWidget", "Timeline Widget" }, - { WindowControl::InfoBar, "InfoBar", "Information Bar" }, - { WindowControl::Tools, "Tools", "Tools" } + { WindowComponent::ToolBar, "ToolBar", "Tool Bar", "", "" }, + { WindowComponent::ToolsPanel, "ToolsPanel", "Tools Panel", "PanelRight", "Toggle the tools panel" }, + { WindowComponent::PlaybackPanel, "PlaybackPanel", "Playback Panel", "PanelBottom", "Toggle the playback panel" } }; std::weak_ptr appWeak(app); - for (const auto& control : controls) + for (const auto& component : components) { - const std::string actionName = dtk::Format("Window/{0}").arg(control.action); + const std::string actionName = dtk::Format("Window/{0}").arg(component.action); _actions[actionName] = std::make_shared( - control.text, - [appWeak, control](bool value) + component.text, + component.icon, + [appWeak, component](bool value) { if (auto app = appWeak.lock()) { - app->getWindowModel()->setControl(control.control, value); + app->getWindowModel()->setComponent(component.component, value); } }); + _actions[actionName]->toolTip = component.tooltip; _menus["Window"]->addItem(_actions[actionName]); } @@ -727,20 +799,16 @@ namespace toucan _menus["Window"]->setItemChecked(_actions["Window/FullScreen"], value); }); - _controlsObserver = dtk::MapObserver::create( - app->getWindowModel()->observeControls(), - [this](const std::map& value) + _componentsObserver = dtk::MapObserver::create( + app->getWindowModel()->observeComponents(), + [this](const std::map& value) { - auto i = value.find(WindowControl::ToolBar); + auto i = value.find(WindowComponent::ToolBar); _menus["Window"]->setItemChecked(_actions["Window/ToolBar"], i->second); - i = value.find(WindowControl::PlaybackBar); - _menus["Window"]->setItemChecked(_actions["Window/PlaybackBar"], i->second); - i = value.find(WindowControl::TimelineWidget); - _menus["Window"]->setItemChecked(_actions["Window/TimelineWidget"], i->second); - i = value.find(WindowControl::InfoBar); - _menus["Window"]->setItemChecked(_actions["Window/InfoBar"], i->second); - i = value.find(WindowControl::Tools); - _menus["Window"]->setItemChecked(_actions["Window/Tools"], i->second); + i = value.find(WindowComponent::ToolsPanel); + _menus["Window"]->setItemChecked(_actions["Window/ToolsPanel"], i->second); + i = value.find(WindowComponent::PlaybackPanel); + _menus["Window"]->setItemChecked(_actions["Window/PlaybackPanel"], i->second); }); _displayScaleObserver = dtk::ValueObserver::create( @@ -761,76 +829,6 @@ namespace toucan }); } - void MenuBar::_viewMenuInit( - const std::shared_ptr& context, - const std::shared_ptr& app) - { - _menus["View"] = dtk::Menu::create(context); - addMenu("View", _menus["View"]); - - _actions["View/ZoomIn"] = std::make_shared( - "Zoom In", - "ViewZoomIn", - dtk::Key::Equal, - 0, - [this] - { - if (_file) - { - _file->getViewModel()->zoomIn(); - } - }); - _actions["View/ZoomIn"]->toolTip = "View zoom in"; - _menus["View"]->addItem(_actions["View/ZoomIn"]); - - _actions["View/ZoomOut"] = std::make_shared( - "Zoom Out", - "ViewZoomOut", - dtk::Key::Minus, - 0, - [this] - { - if (_file) - { - _file->getViewModel()->zoomOut(); - } - }); - _actions["View/ZoomOut"]->toolTip = "View zoom out"; - _menus["View"]->addItem(_actions["View/ZoomOut"]); - - _actions["View/ZoomReset"] = std::make_shared( - "Zoom Reset", - "ViewZoomReset", - dtk::Key::_0, - 0, - [this] - { - if (_file) - { - _file->getViewModel()->zoomReset(); - } - }); - _actions["View/ZoomReset"]->toolTip = "Reset the view zoom"; - _menus["View"]->addItem(_actions["View/ZoomReset"]); - - _menus["View"]->addDivider(); - - _actions["View/Frame"] = std::make_shared( - "Frame View", - "ViewFrame", - dtk::Key::Backspace, - 0, - [this](bool value) - { - if (_file) - { - _file->getViewModel()->setFrame(value); - } - }); - _actions["View/Frame"]->toolTip = "Frame the view"; - _menus["View"]->addItem(_actions["View/Frame"]); - } - void MenuBar::_fileMenuUpdate() { const bool file = _file.get(); @@ -887,17 +885,13 @@ namespace toucan _menus["Playback"]->setItemEnabled(_actions["Playback/Toggle"], file); } - void MenuBar::_windowMenuUpdate() - { - } - void MenuBar::_viewMenuUpdate() { const bool file = _file.get(); if (file) { _frameViewObserver = dtk::ValueObserver::create( - _file->getViewModel()->observeFrame(), + _file->getViewModel()->observeFrameView(), [this](bool value) { _menus["View"]->setItemChecked(_actions["View/Frame"], value); @@ -913,4 +907,8 @@ namespace toucan _menus["View"]->setItemEnabled(_actions["View/ZoomReset"], file); _menus["View"]->setItemEnabled(_actions["View/Frame"], file); } + + void MenuBar::_windowMenuUpdate() + { + } } diff --git a/lib/toucanView/MenuBar.h b/lib/toucanView/MenuBar.h index b71b5a5..e073bb4 100644 --- a/lib/toucanView/MenuBar.h +++ b/lib/toucanView/MenuBar.h @@ -19,6 +19,7 @@ namespace toucan class FilesModel; class MainWindow; + //! Menu bar. class MenuBar : public dtk::MenuBar { protected: @@ -31,12 +32,14 @@ namespace toucan public: virtual ~MenuBar(); + //! Create a new menu bar. static std::shared_ptr create( const std::shared_ptr&, const std::shared_ptr&, const std::shared_ptr&, const std::shared_ptr& parent = nullptr); + //! Get the actions. const std::map >& getActions() const; private: @@ -52,20 +55,20 @@ namespace toucan void _playbackMenuInit( const std::shared_ptr&, const std::shared_ptr&); + void _viewMenuInit( + const std::shared_ptr&, + const std::shared_ptr&); void _windowMenuInit( const std::shared_ptr&, const std::shared_ptr&, const std::shared_ptr&); - void _viewMenuInit( - const std::shared_ptr&, - const std::shared_ptr&); void _fileMenuUpdate(); void _selectMenuUpdate(); void _timeMenuUpdate(); void _playbackMenuUpdate(); - void _windowMenuUpdate(); void _viewMenuUpdate(); + void _windowMenuUpdate(); std::weak_ptr _app; std::shared_ptr _filesModel; @@ -81,11 +84,11 @@ namespace toucan std::shared_ptr > _fileIndexObserver; std::shared_ptr > _recentFilesObserver; std::shared_ptr > _playbackObserver; + std::shared_ptr > _frameViewObserver; std::shared_ptr > _fullScreenObserver; - std::shared_ptr > _controlsObserver; + std::shared_ptr > _componentsObserver; std::shared_ptr > _displayScaleObserver; std::shared_ptr > _tooltipsObserver; - std::shared_ptr > _frameViewObserver; }; } diff --git a/lib/toucanView/PlaybackBar.cpp b/lib/toucanView/PlaybackBar.cpp index 34d9701..2c70afc 100644 --- a/lib/toucanView/PlaybackBar.cpp +++ b/lib/toucanView/PlaybackBar.cpp @@ -18,6 +18,7 @@ namespace toucan _layout = dtk::HorizontalLayout::create(context, shared_from_this()); _layout->setMarginRole(dtk::SizeRole::MarginInside); _layout->setSpacingRole(dtk::SizeRole::SpacingSmall); + _layout->setVAlign(dtk::VAlign::Center); _playbackButtons = PlaybackButtons::create(context, _layout); @@ -33,6 +34,7 @@ namespace toucan context, { "Timecode", "Frames", "Seconds" }, _layout); + _timeUnitsComboBox->setTooltip("Set the time units"); _frameButtons->setCallback( [this](TimeAction value) diff --git a/lib/toucanView/PlaybackBar.h b/lib/toucanView/PlaybackBar.h index 25694a5..d035b43 100644 --- a/lib/toucanView/PlaybackBar.h +++ b/lib/toucanView/PlaybackBar.h @@ -15,6 +15,7 @@ namespace toucan class App; class File; + //! Playback tool bar. class PlaybackBar : public dtk::IWidget { protected: @@ -26,6 +27,7 @@ namespace toucan public: virtual ~PlaybackBar(); + //! Create a new tool bar. static std::shared_ptr create( const std::shared_ptr&, const std::shared_ptr&, diff --git a/lib/toucanView/StackItem.h b/lib/toucanView/StackItem.h index 6399dd3..6d85c05 100644 --- a/lib/toucanView/StackItem.h +++ b/lib/toucanView/StackItem.h @@ -9,6 +9,7 @@ namespace toucan { + //! Timeline stack item. class StackItem : public IItem { protected: @@ -21,6 +22,7 @@ namespace toucan public: virtual ~StackItem(); + //! Create a new item. static std::shared_ptr create( const std::shared_ptr&, const std::shared_ptr&, diff --git a/lib/toucanView/ThumbnailGenerator.h b/lib/toucanView/ThumbnailGenerator.h index 87fd6b2..91fb092 100644 --- a/lib/toucanView/ThumbnailGenerator.h +++ b/lib/toucanView/ThumbnailGenerator.h @@ -18,6 +18,7 @@ namespace toucan { + //! Thumbnail request. struct ThumbnailRequest { uint64_t id = 0; @@ -26,6 +27,7 @@ namespace toucan std::future > future; }; + //! Thumbnail generator. class ThumbnailGenerator : public std::enable_shared_from_this { public: @@ -36,12 +38,15 @@ namespace toucan ~ThumbnailGenerator(); + //! Get the aspect ratio. float getAspect() const; + //! Get a thumbnail. ThumbnailRequest getThumbnail( const OTIO_NS::RationalTime&, int height); + //! Cancel thumbnail requests. void cancelThumbnails(const std::vector&); private: diff --git a/lib/toucanView/TimeUnitsModel.cpp b/lib/toucanView/TimeUnitsModel.cpp index e9d96d4..63e77c8 100644 --- a/lib/toucanView/TimeUnitsModel.cpp +++ b/lib/toucanView/TimeUnitsModel.cpp @@ -4,6 +4,8 @@ #include "TimeUnitsModel.h" #include +#include +#include #include @@ -12,44 +14,26 @@ namespace toucan { - namespace - { - const std::array timeUnits = - { - "Timecode", - "Frames", - "Seconds" - }; - } - - std::string toString(TimeUnits value) - { - return timeUnits[static_cast(value)]; - } - - TimeUnits fromString(const std::string& value) - { - const auto i = std::find(timeUnits.begin(), timeUnits.end(), value); - return i != timeUnits.end() ? - static_cast(i - timeUnits.begin()) : - TimeUnits::Timecode; - } + DTK_ENUM_IMPL( + TimeUnits, + "Timecode", + "Frames", + "Seconds"); TimeUnitsModel::TimeUnitsModel(const std::shared_ptr& context) { _context = context; + TimeUnits value = TimeUnits::Timecode; try { auto settings = context->getSystem(); const auto json = std::any_cast(settings->get("TimeUnits")); - if (json.is_object()) + auto i = json.find("Units"); + if (i != json.end() && i->is_string()) { - auto i = json.find("Units"); - if (i != json.end()) - { - value = toucan::fromString(i->get()); - } + std::stringstream ss(i->get()); + ss >> value; } } catch (const std::exception&) @@ -61,7 +45,9 @@ namespace toucan TimeUnitsModel::~TimeUnitsModel() { nlohmann::json json; - json["Units"] = toucan::toString(_timeUnits->get()); + std::stringstream ss; + ss << _timeUnits->get(); + json["Units"] = ss.str(); auto context = _context.lock(); auto settings = context->getSystem(); settings->set("TimeUnits", json); diff --git a/lib/toucanView/TimeUnitsModel.h b/lib/toucanView/TimeUnitsModel.h index 5303e06..e1d27d4 100644 --- a/lib/toucanView/TimeUnitsModel.h +++ b/lib/toucanView/TimeUnitsModel.h @@ -15,8 +15,12 @@ namespace toucan { Timecode, Frames, - Seconds + Seconds, + + Count, + First = Timecode }; + DTK_ENUM(TimeUnits); //! Convert to a string. std::string toString(TimeUnits); diff --git a/lib/toucanView/TimeWidgets.cpp b/lib/toucanView/TimeWidgets.cpp index 10800b8..1534245 100644 --- a/lib/toucanView/TimeWidgets.cpp +++ b/lib/toucanView/TimeWidgets.cpp @@ -177,6 +177,7 @@ namespace toucan _layout->setSpacingRole(dtk::SizeRole::SpacingTool); _lineEdit = dtk::LineEdit::create(context, _layout); + _lineEdit->setFontRole(dtk::FontRole::Mono); _lineEdit->setFormat("00:00:00:00"); _incButtons = dtk::IncButtons::create(context, _layout); @@ -333,6 +334,7 @@ namespace toucan _timeUnitsModel = timeUnitsModel; _label = dtk::Label::create(context, shared_from_this()); + _label->setFontRole(dtk::FontRole::Mono); _label->setMarginRole(dtk::SizeRole::MarginInside); _timeUpdate(); diff --git a/lib/toucanView/TimeWidgets.h b/lib/toucanView/TimeWidgets.h index f95a34d..de22e8b 100644 --- a/lib/toucanView/TimeWidgets.h +++ b/lib/toucanView/TimeWidgets.h @@ -16,6 +16,7 @@ namespace toucan { + //! Frame buttons. class FrameButtons : public dtk::IWidget { protected: @@ -26,10 +27,12 @@ namespace toucan public: virtual ~FrameButtons(); + //! Create a new widget. static std::shared_ptr create( const std::shared_ptr&, const std::shared_ptr& parent = nullptr); + //! Set the callback. void setCallback(const std::function&); void setGeometry(const dtk::Box2I&) override; @@ -41,6 +44,7 @@ namespace toucan std::shared_ptr _buttonGroup; }; + //! Playback buttons. class PlaybackButtons : public dtk::IWidget { protected: @@ -51,12 +55,15 @@ namespace toucan public: virtual ~PlaybackButtons(); + //! Create a new widget. static std::shared_ptr create( const std::shared_ptr&, const std::shared_ptr& parent = nullptr); + //! Set the playback. void setPlayback(Playback); + //! Set the callback. void setCallback(const std::function&); void setGeometry(const dtk::Box2I&) override; @@ -71,6 +78,7 @@ namespace toucan std::shared_ptr _buttonGroup; }; + //! Time edit. class TimeEdit : public dtk::IWidget { protected: @@ -82,14 +90,19 @@ namespace toucan public: virtual ~TimeEdit(); + //! Create a new widget. static std::shared_ptr create( const std::shared_ptr&, const std::shared_ptr& timeUnitsModel, const std::shared_ptr& parent = nullptr); + //! Set the time. void setTime(const OTIO_NS::RationalTime&); + + //! Set the time range. void setTimeRange(const OTIO_NS::TimeRange&); + //! Set the callback. void setCallback(const std::function&); void setGeometry(const dtk::Box2I&) override; @@ -114,6 +127,7 @@ namespace toucan std::shared_ptr > _timeUnitsObserver; }; + //! Time label. class TimeLabel : public dtk::IWidget { protected: @@ -125,13 +139,16 @@ namespace toucan public: virtual ~TimeLabel(); + //! Create a new widget. static std::shared_ptr create( const std::shared_ptr&, const std::shared_ptr& timeUnitsModel, const std::shared_ptr& parent = nullptr); + //! Set the time. void setTime(const OTIO_NS::RationalTime&); + //! Set the margin size role. void setMarginRole(dtk::SizeRole); void setGeometry(const dtk::Box2I&) override; diff --git a/lib/toucanView/TimelineItem.cpp b/lib/toucanView/TimelineItem.cpp index 1a367b9..5194ab4 100644 --- a/lib/toucanView/TimelineItem.cpp +++ b/lib/toucanView/TimelineItem.cpp @@ -154,7 +154,7 @@ namespace toucan _size.handle = event.style->getSizeRole(dtk::SizeRole::Handle, event.displayScale); _size.thumbnailSize.h = 2 * event.style->getSizeRole(dtk::SizeRole::SwatchLarge, event.displayScale); _size.thumbnailSize.w = _size.thumbnailSize.h * _thumbnailGenerator->getAspect(); - _size.fontInfo = event.style->getFontRole(dtk::FontRole::Label, event.displayScale); + _size.fontInfo = event.style->getFontRole(dtk::FontRole::Mono, event.displayScale); _size.fontMetrics = event.fontSystem->getMetrics(_size.fontInfo); std::vector ids; for (const auto& request : _thumbnailRequests) diff --git a/lib/toucanView/TimelineItem.h b/lib/toucanView/TimelineItem.h index 186a136..b5664b2 100644 --- a/lib/toucanView/TimelineItem.h +++ b/lib/toucanView/TimelineItem.h @@ -15,6 +15,7 @@ namespace toucan class File; class SelectionModel; + //! Timeline item. class TimelineItem : public IItem { protected: @@ -27,16 +28,23 @@ namespace toucan public: virtual ~TimelineItem(); + //! Create a new item. static std::shared_ptr create( const std::shared_ptr&, const std::shared_ptr&, const std::shared_ptr&, const std::shared_ptr& parent = nullptr); + //! Get the current time. const OTIO_NS::RationalTime& getCurrentTime() const; + + //! Set the current time. void setCurrentTime(const OTIO_NS::RationalTime&); + + //! Set the current time callback. void setCurrentTimeCallback(const std::function&); + //! Set the in/out range. void setInOutRange(const OTIO_NS::TimeRange&); void setGeometry(const dtk::Box2I&) override; diff --git a/lib/toucanView/TimelineWidget.cpp b/lib/toucanView/TimelineWidget.cpp index 25e71da..8bd3ad9 100644 --- a/lib/toucanView/TimelineWidget.cpp +++ b/lib/toucanView/TimelineWidget.cpp @@ -33,6 +33,7 @@ namespace toucan _scrollWidget = dtk::ScrollWidget::create(context, dtk::ScrollType::Both, shared_from_this()); _scrollWidget->setScrollEventsEnabled(false); _scrollWidget->setBorder(false); + _scrollWidget->setScrollBarsVisible(false); auto appWeak = std::weak_ptr(app); _fileObserver = dtk::ValueObserver >::create( diff --git a/lib/toucanView/TimelineWidget.h b/lib/toucanView/TimelineWidget.h index 7a977b0..de9c9b3 100644 --- a/lib/toucanView/TimelineWidget.h +++ b/lib/toucanView/TimelineWidget.h @@ -16,6 +16,7 @@ namespace toucan class File; class TimelineItem; + //! Timeline widget. class TimelineWidget : public dtk::IWidget { protected: @@ -27,17 +28,28 @@ namespace toucan public: virtual ~TimelineWidget(); + //! Create a new widget. static std::shared_ptr create( const std::shared_ptr&, const std::shared_ptr&, const std::shared_ptr& parent = nullptr); + //! Set the view zoom. void setViewZoom(double); + + //! Set the view zoom. void setViewZoom(double, const dtk::V2I& focus); + //! Get whether frame view is enabled. bool hasFrameView() const; + + //! Frame the view. void frameView(); + + //! Observe whether frame view is enabled. std::shared_ptr > observeFrameView() const; + + //! Set whether frame view is enabled. void setFrameView(bool); void setGeometry(const dtk::Box2I&) override; diff --git a/lib/toucanView/ToolBar.cpp b/lib/toucanView/ToolBar.cpp index 7457fae..8b2fe36 100644 --- a/lib/toucanView/ToolBar.cpp +++ b/lib/toucanView/ToolBar.cpp @@ -49,25 +49,6 @@ namespace toucan _buttons[name] = button; } - dtk::Divider::create(context, dtk::Orientation::Horizontal, _layout); - - hLayout = dtk::HorizontalLayout::create(context, _layout); - hLayout->setSpacingRole(dtk::SizeRole::SpacingTool); - auto i = actions.find("Window/FullScreen"); - auto button = dtk::ToolButton::create(context, hLayout); - button->setIcon(i->second->icon); - button->setCheckable(true); - button->setTooltip(i->second->toolTip); - button->setCheckedCallback( - [i](bool value) - { - if (i->second->checkedCallback) - { - i->second->checkedCallback(value); - } - }); - _buttons["Window/FullScreen"] = button; - dtk::Divider::create(context, dtk::Orientation::Horizontal, _layout); hLayout = dtk::HorizontalLayout::create(context, _layout); @@ -80,8 +61,8 @@ namespace toucan }; for (const auto& name : actionNames) { - i = actions.find(name); - button = dtk::ToolButton::create(context, hLayout); + auto i = actions.find(name); + auto button = dtk::ToolButton::create(context, hLayout); button->setIcon(i->second->icon); button->setTooltip(i->second->toolTip); button->setClickedCallback( @@ -95,8 +76,8 @@ namespace toucan _buttons[name] = button; } - i = actions.find("View/Frame"); - button = dtk::ToolButton::create(context, hLayout); + auto i = actions.find("View/Frame"); + auto button = dtk::ToolButton::create(context, hLayout); button->setIcon(i->second->icon); button->setCheckable(true); button->setTooltip(i->second->toolTip); @@ -110,6 +91,56 @@ namespace toucan }); _buttons["View/Frame"] = button; + dtk::Divider::create(context, dtk::Orientation::Horizontal, _layout); + + hLayout = dtk::HorizontalLayout::create(context, _layout); + hLayout->setSpacingRole(dtk::SizeRole::SpacingTool); + + i = actions.find("Window/FullScreen"); + button = dtk::ToolButton::create(context, hLayout); + button->setIcon(i->second->icon); + button->setCheckable(true); + button->setTooltip(i->second->toolTip); + button->setCheckedCallback( + [i](bool value) + { + if (i->second->checkedCallback) + { + i->second->checkedCallback(value); + } + }); + _buttons["Window/FullScreen"] = button; + + i = actions.find("Window/ToolsPanel"); + button = dtk::ToolButton::create(context, hLayout); + button->setIcon(i->second->icon); + button->setCheckable(true); + button->setTooltip(i->second->toolTip); + button->setCheckedCallback( + [i](bool value) + { + if (i->second->checkedCallback) + { + i->second->checkedCallback(value); + } + }); + _buttons["Window/ToolsPanel"] = button; + + i = actions.find("Window/PlaybackPanel"); + button = dtk::ToolButton::create(context, hLayout); + button->setIcon(i->second->icon); + button->setCheckable(true); + button->setTooltip(i->second->toolTip); + button->setCheckedCallback( + [i](bool value) + { + if (i->second->checkedCallback) + { + i->second->checkedCallback(value); + } + }); + _buttons["Window/PlaybackPanel"] = button; + _widgetUpdate(); _filesObserver = dtk::ListObserver >::create( @@ -134,6 +165,16 @@ namespace toucan { _buttons["Window/FullScreen"]->setChecked(value); }); + + _componentObserver = dtk::MapObserver::create( + app->getWindowModel()->observeComponents(), + [this](const std::map value) + { + auto i = value.find(WindowComponent::ToolsPanel); + _buttons["Window/ToolsPanel"]->setChecked(i != value.end() ? i->second : false); + i = value.find(WindowComponent::PlaybackPanel); + _buttons["Window/PlaybackPanel"]->setChecked(i != value.end() ? i->second : false); + }); } ToolBar::~ToolBar() @@ -175,7 +216,7 @@ namespace toucan if (_file) { _frameViewObserver = dtk::ValueObserver::create( - _file->getViewModel()->observeFrame(), + _file->getViewModel()->observeFrameView(), [this](bool value) { _buttons["View/Frame"]->setChecked(value); diff --git a/lib/toucanView/ToolBar.h b/lib/toucanView/ToolBar.h index d88cdd4..dd151b5 100644 --- a/lib/toucanView/ToolBar.h +++ b/lib/toucanView/ToolBar.h @@ -3,6 +3,8 @@ #pragma once +#include "WindowModel.h" + #include #include #include @@ -15,6 +17,7 @@ namespace toucan class File; class MainWindow; + //! Tool bar. class ToolBar : public dtk::IWidget { protected: @@ -28,6 +31,7 @@ namespace toucan public: virtual ~ToolBar(); + //! Create a new tool bar. static std::shared_ptr create( const std::shared_ptr&, const std::shared_ptr&, @@ -49,8 +53,9 @@ namespace toucan std::shared_ptr > > _filesObserver; std::shared_ptr > > _fileObserver; - std::shared_ptr > _fullScreenObserver; std::shared_ptr > _frameViewObserver; + std::shared_ptr > _fullScreenObserver; + std::shared_ptr > _componentObserver; }; } diff --git a/lib/toucanView/TrackItem.h b/lib/toucanView/TrackItem.h index 3418882..ecdbfff 100644 --- a/lib/toucanView/TrackItem.h +++ b/lib/toucanView/TrackItem.h @@ -9,6 +9,7 @@ namespace toucan { + //! Timeline track item. class TrackItem : public IItem { protected: @@ -21,6 +22,7 @@ namespace toucan public: virtual ~TrackItem(); + //! Create a new item. static std::shared_ptr create( const std::shared_ptr&, const std::shared_ptr&, diff --git a/lib/toucanView/ViewModel.cpp b/lib/toucanView/ViewModel.cpp index 5f2e59d..1e695e3 100644 --- a/lib/toucanView/ViewModel.cpp +++ b/lib/toucanView/ViewModel.cpp @@ -12,7 +12,7 @@ namespace toucan _zoomIn = dtk::ObservableValue::create(false); _zoomOut = dtk::ObservableValue::create(false); _zoomReset = dtk::ObservableValue::create(false); - _frame = dtk::ObservableValue::create(true); + _frameView = dtk::ObservableValue::create(true); } ViewModel::~ViewModel() @@ -48,18 +48,18 @@ namespace toucan return _zoomReset; } - bool ViewModel::getFrame() const + bool ViewModel::getFrameView() const { - return _frame->get(); + return _frameView->get(); } - std::shared_ptr > ViewModel::observeFrame() const + std::shared_ptr > ViewModel::observeFrameView() const { - return _frame; + return _frameView; } - void ViewModel::setFrame(bool value) + void ViewModel::setFrameView(bool value) { - _frame->setIfChanged(value); + _frameView->setIfChanged(value); } } diff --git a/lib/toucanView/ViewModel.h b/lib/toucanView/ViewModel.h index eb9a2db..90d05d1 100644 --- a/lib/toucanView/ViewModel.h +++ b/lib/toucanView/ViewModel.h @@ -9,6 +9,7 @@ namespace toucan { + //! View model. class ViewModel : public std::enable_shared_from_this { public: @@ -16,21 +17,37 @@ namespace toucan virtual ~ViewModel(); + //! Zoom in. void zoomIn(); + + //! Zoom out. void zoomOut(); + + //! Reset the zoom. void zoomReset(); + + //! Observe the zoom in. std::shared_ptr > observeZoomIn() const; + + //! Observe the zoom out. std::shared_ptr > observeZoomOut() const; + + //! Observe the zoom reset. std::shared_ptr > observeZoomReset() const; - bool getFrame() const; - std::shared_ptr > observeFrame() const; - void setFrame(bool); + //! Get whether frame view is enabled. + bool getFrameView() const; + + //! Observe whether frame view is enabled. + std::shared_ptr > observeFrameView() const; + + //! Set whether frame view is enabled. + void setFrameView(bool); private: std::shared_ptr > _zoomIn; std::shared_ptr > _zoomOut; std::shared_ptr > _zoomReset; - std::shared_ptr > _frame; + std::shared_ptr > _frameView; }; } diff --git a/lib/toucanView/Viewport.cpp b/lib/toucanView/Viewport.cpp index d8a85a4..5f67c77 100644 --- a/lib/toucanView/Viewport.cpp +++ b/lib/toucanView/Viewport.cpp @@ -20,9 +20,9 @@ namespace toucan _setMousePressEnabled(true); _viewModel = file->getViewModel(); - _pos = dtk::ObservableValue::create(); - _zoom = dtk::ObservableValue::create(1.F); - _frame = dtk::ObservableValue::create(true); + _viewPos = dtk::ObservableValue::create(); + _viewZoom = dtk::ObservableValue::create(1.F); + _frameView = dtk::ObservableValue::create(true); _imageObserver = dtk::ValueObserver >::create( file->observeCurrentImage(), @@ -38,7 +38,7 @@ namespace toucan { if (value) { - zoomIn(); + viewZoomIn(); } }); @@ -48,7 +48,7 @@ namespace toucan { if (value) { - zoomOut(); + viewZoomOut(); } }); @@ -58,15 +58,15 @@ namespace toucan { if (value) { - zoomReset(); + viewZoomReset(); } }); _frameObserver = dtk::ValueObserver::create( - _viewModel->observeFrame(), + _viewModel->observeFrameView(), [this](bool value) { - setFrame(value); + setFrameView(value); }); } @@ -83,83 +83,83 @@ namespace toucan return out; } - const dtk::V2I& Viewport::getPos() const + const dtk::V2I& Viewport::getViewPos() const { - return _pos->get(); + return _viewPos->get(); } - float Viewport::getZoom() const + float Viewport::getViewZoom() const { - return _zoom->get(); + return _viewZoom->get(); } - std::shared_ptr > Viewport::observePos() const + std::shared_ptr > Viewport::observeViewPos() const { - return _pos; + return _viewPos; } - std::shared_ptr > Viewport::observeZoom() const + std::shared_ptr > Viewport::observeViewZoom() const { - return _zoom; + return _viewZoom; } - void Viewport::setPosZoom(const dtk::V2I& pos, float zoom) + void Viewport::setViewPosZoom(const dtk::V2I& pos, float zoom) { - setFrame(false); - bool changed = _pos->setIfChanged(pos); - changed |= _zoom->setIfChanged(zoom); + setFrameView(false); + bool changed = _viewPos->setIfChanged(pos); + changed |= _viewZoom->setIfChanged(zoom); if (changed) { _setDrawUpdate(); } } - void Viewport::setZoom(float value) + void Viewport::setViewZoom(float value) { const dtk::Box2I& g = getGeometry(); const dtk::V2I viewportCenter(g.w() / 2, g.h() / 2); - setZoom(value, _isMouseInside() ? (_getMousePos() - g.min) : viewportCenter); + setViewZoom(value, _isMouseInside() ? (_getMousePos() - g.min) : viewportCenter); } - void Viewport::setZoom(float zoom, const dtk::V2I& focus) + void Viewport::setViewZoom(float zoom, const dtk::V2I& focus) { - dtk::V2I pos = _pos->get(); - const float zoomPrev = _zoom->get(); + dtk::V2I pos = _viewPos->get(); + const float zoomPrev = _viewZoom->get(); pos.x = focus.x + (pos.x - focus.x) * (zoom / zoomPrev); pos.y = focus.y + (pos.y - focus.y) * (zoom / zoomPrev); - setPosZoom(pos, zoom); + setViewPosZoom(pos, zoom); } - void Viewport::zoomIn(double amount) + void Viewport::viewZoomIn(double amount) { - setZoom(_zoom->get() * amount); + setViewZoom(_viewZoom->get() * amount); } - void Viewport::zoomOut(double amount) + void Viewport::viewZoomOut(double amount) { - setZoom(_zoom->get() / amount); + setViewZoom(_viewZoom->get() / amount); } - void Viewport::zoomReset() + void Viewport::viewZoomReset() { - setZoom(1.F); + setViewZoom(1.F); } - bool Viewport::getFrame() const + bool Viewport::getFrameView() const { - return _frame->get(); + return _frameView->get(); } - std::shared_ptr > Viewport::observeFrame() const + std::shared_ptr > Viewport::observeFrameView() const { - return _frame; + return _frameView; } - void Viewport::setFrame(bool value) + void Viewport::setFrameView(bool value) { - if (_frame->setIfChanged(value)) + if (_frameView->setIfChanged(value)) { - _viewModel->setFrame(value); + _viewModel->setFrameView(value); _setDrawUpdate(); } } @@ -173,15 +173,15 @@ namespace toucan dtk::Color4F(0.F, 0.F, 0.F)); if (_image) { - if (_frame->get()) + if (_frameView->get()) { _frameUpdate(); } const dtk::Size2I& imageSize = _image->getSize(); dtk::M44F vm; vm = vm * dtk::translate(dtk::V3F(g.min.x, g.min.y, 0.F)); - vm = vm * dtk::translate(dtk::V3F(_pos->get().x, _pos->get().y, 0.F)); - vm = vm * dtk::scale(dtk::V3F(_zoom->get(), _zoom->get(), 1.F)); + vm = vm * dtk::translate(dtk::V3F(_viewPos->get().x, _viewPos->get().y, 0.F)); + vm = vm * dtk::scale(dtk::V3F(_viewZoom->get(), _viewZoom->get(), 1.F)); const auto m = event.render->getTransform(); event.render->setTransform(m * vm); dtk::ImageOptions options; @@ -202,7 +202,7 @@ namespace toucan { event.accept = true; const dtk::V2I& mousePressPos = _getMousePressPos(); - _pos->setIfChanged(dtk::V2I( + _viewPos->setIfChanged(dtk::V2I( _viewMousePress.x + (event.pos.x - mousePressPos.x), _viewMousePress.y + (event.pos.y - mousePressPos.y))); _setDrawUpdate(); @@ -215,8 +215,8 @@ namespace toucan if (0 == event.button && 0 == event.modifiers) { event.accept = true; - setFrame(false); - _viewMousePress = _pos->get(); + setFrameView(false); + _viewMousePress = _viewPos->get(); } } @@ -234,18 +234,18 @@ namespace toucan event.accept = true; if (event.value.y > 0) { - zoomOut(.9F); + viewZoomOut(.9F); } else if (event.value.y < 0) { - zoomIn(.9F); + viewZoomIn(.9F); } } } void Viewport::_frameUpdate() { - if (_frame && _image) + if (_frameView->get() && _image) { const dtk::Box2I& g = getGeometry(); const dtk::Size2I imageSize = _image->getSize(); @@ -259,8 +259,8 @@ namespace toucan g.w() / 2.F - c.x * zoom, g.h() / 2.F - c.y * zoom); - _pos->setIfChanged(pos); - _zoom->setIfChanged(zoom); + _viewPos->setIfChanged(pos); + _viewZoom->setIfChanged(zoom); } } } diff --git a/lib/toucanView/Viewport.h b/lib/toucanView/Viewport.h index 6994559..2ec5398 100644 --- a/lib/toucanView/Viewport.h +++ b/lib/toucanView/Viewport.h @@ -12,6 +12,7 @@ namespace toucan class File; class ViewModel; + //! Viewport widget. class Viewport : public dtk::IWidget { protected: @@ -23,25 +24,50 @@ namespace toucan public: virtual ~Viewport(); + //! Create a new widget. static std::shared_ptr create( const std::shared_ptr&, const std::shared_ptr&, const std::shared_ptr& parent = nullptr); - const dtk::V2I& getPos() const; - float getZoom() const; - std::shared_ptr > observePos() const; - std::shared_ptr > observeZoom() const; - void setPosZoom(const dtk::V2I&, float); - void setZoom(float); - void setZoom(float, const dtk::V2I& focus); - void zoomIn(double amount = 2.F); - void zoomOut(double amount = 2.F); - void zoomReset(); - - bool getFrame() const; - std::shared_ptr > observeFrame() const; - void setFrame(bool); + //! Get the view position. + const dtk::V2I& getViewPos() const; + + //! Get the view zoom. + float getViewZoom() const; + + //! Observe the view position. + std::shared_ptr > observeViewPos() const; + + //! Observe the view zoom. + std::shared_ptr > observeViewZoom() const; + + //! Set the view position and zoom. + void setViewPosZoom(const dtk::V2I&, float); + + //! Set the view zoom. + void setViewZoom(float); + + //! Set the view zoom. + void setViewZoom(float, const dtk::V2I& focus); + + //! Zoom in the view. + void viewZoomIn(double amount = 2.F); + + //! Zoom out the view. + void viewZoomOut(double amount = 2.F); + + //! Reset the view zoom. + void viewZoomReset(); + + //! Get whether frame view is enabled. + bool getFrameView() const; + + //! Observe whether frame view is enabled. + std::shared_ptr > observeFrameView() const; + + //! Set whether frame view is enabled. + void setFrameView(bool); void drawEvent(const dtk::Box2I&, const dtk::DrawEvent&) override; void mouseMoveEvent(dtk::MouseMoveEvent&) override; @@ -54,9 +80,9 @@ namespace toucan std::shared_ptr _viewModel; std::shared_ptr _image; - std::shared_ptr > _pos; - std::shared_ptr > _zoom; - std::shared_ptr > _frame; + std::shared_ptr > _viewPos; + std::shared_ptr > _viewZoom; + std::shared_ptr > _frameView; dtk::V2I _viewMousePress; std::shared_ptr > > _imageObserver; diff --git a/lib/toucanView/WindowModel.cpp b/lib/toucanView/WindowModel.cpp index ac10ac2..903495b 100644 --- a/lib/toucanView/WindowModel.cpp +++ b/lib/toucanView/WindowModel.cpp @@ -3,48 +3,98 @@ #include "WindowModel.h" +#include +#include +#include + +#include + +#include + namespace toucan { - WindowModel::WindowModel() + DTK_ENUM_IMPL( + WindowComponent, + "ToolBar", + "ToolsPanel", + "PlaybackPanel"); + + WindowModel::WindowModel(const std::shared_ptr& context) { - std::map values = + _context = context; + + std::map components = { - { WindowControl::ToolBar, true }, - { WindowControl::PlaybackBar, true }, - { WindowControl::TimelineWidget, true }, - { WindowControl::InfoBar, true }, - { WindowControl::Tools, true } + { WindowComponent::ToolBar, true }, + { WindowComponent::ToolsPanel, true }, + { WindowComponent::PlaybackPanel, true } }; - _controls = dtk::ObservableMap::create(values); - _tooltips = dtk::ObservableValue::create(true); + bool tooltips = true; + try + { + auto settings = context->getSystem(); + const auto json = std::any_cast(settings->get("WindowModel")); + for (auto& i : components) + { + std::stringstream ss; + ss << i.first; + auto j = json.find(ss.str()); + if (j != json.end() && j->is_boolean()) + { + i.second = j->get(); + } + } + auto i = json.find("Tooltips"); + if (i != json.end() && i->is_boolean()) + { + tooltips = i->get(); + } + } + catch (const std::exception&) + {} + + _components = dtk::ObservableMap::create(components); + _tooltips = dtk::ObservableValue::create(tooltips); } WindowModel::~WindowModel() - {} + { + nlohmann::json json; + for (const auto i : _components->get()) + { + std::stringstream ss; + ss << i.first; + json[ss.str()] = i.second; + } + json["Tooltips"] = _tooltips->get(); + auto context = _context.lock(); + auto settings = context->getSystem(); + settings->set("WindowModel", json); + } - const std::map WindowModel::getControls() const + const std::map WindowModel::getComponents() const { - return _controls->get(); + return _components->get(); } - std::shared_ptr > WindowModel::observeControls() const + std::shared_ptr > WindowModel::observeComponents() const { - return _controls; + return _components; } - void WindowModel::setControls(const std::map& value) + void WindowModel::setComponents(const std::map& value) { - _controls->setIfChanged(value); + _components->setIfChanged(value); } - bool WindowModel::getControl(WindowControl value) const + bool WindowModel::getComponent(WindowComponent value) const { - return _controls->getItem(value); + return _components->getItem(value); } - void WindowModel::setControl(WindowControl control, bool value) + void WindowModel::setComponent(WindowComponent component, bool value) { - _controls->setItemOnlyIfChanged(control, value); + _components->setItemOnlyIfChanged(component, value); } bool WindowModel::getTooltips() const diff --git a/lib/toucanView/WindowModel.h b/lib/toucanView/WindowModel.h index b0f6a41..2263cc1 100644 --- a/lib/toucanView/WindowModel.h +++ b/lib/toucanView/WindowModel.h @@ -3,40 +3,59 @@ #pragma once +#include #include #include namespace toucan { - enum class WindowControl + //! Window components. + enum class WindowComponent { ToolBar, - PlaybackBar, - TimelineWidget, - InfoBar, - Tools + ToolsPanel, + PlaybackPanel, + + Count, + First = ToolBar }; + DTK_ENUM(WindowComponent); + //! Window model. class WindowModel : public std::enable_shared_from_this { public: - WindowModel(); + WindowModel(const std::shared_ptr&); virtual ~WindowModel(); - const std::map getControls() const; - std::shared_ptr > observeControls() const; - void setControls(const std::map&); + //! Get the window components. + const std::map getComponents() const; + + //! Observe the window components. + std::shared_ptr > observeComponents() const; + + //! Set the window components. + void setComponents(const std::map&); - bool getControl(WindowControl) const; - void setControl(WindowControl, bool); + //! Get a window component. + bool getComponent(WindowComponent) const; + //! Set a window component. + void setComponent(WindowComponent, bool); + + //! Get whether tooltips are enabled. bool getTooltips() const; + + //! Observe whether tooltips are enabled. std::shared_ptr > observeTooltips() const; + + //! Set whether tooltips are enabled. void setTooltips(bool); private: - std::shared_ptr > _controls; + std::weak_ptr _context; + std::shared_ptr > _components; std::shared_ptr > _tooltips; }; }