diff --git a/CMakeLists.txt b/CMakeLists.txt index b7dd91a15..5e5203782 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,13 +8,13 @@ set(CMAKE_AUTOMOC ON) ### generic info set ( WEBSITE "https://drawpile.net/" ) -set ( DRAWPILE_VERSION "2.0.11" ) +set ( DRAWPILE_VERSION "2.1.0" ) ### protocol versions # see doc/protocol.md for protocol version history set ( DRAWPILE_PROTO_SERVER_VERSION 4 ) -set ( DRAWPILE_PROTO_MAJOR_VERSION 20 ) -set ( DRAWPILE_PROTO_MINOR_VERSION 1 ) +set ( DRAWPILE_PROTO_MAJOR_VERSION 21 ) +set ( DRAWPILE_PROTO_MINOR_VERSION 2 ) set ( DRAWPILE_PROTO_DEFAULT_PORT 27750 ) ### diff --git a/ChangeLog b/ChangeLog index ebe4597a0..cc6295325 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,32 @@ +2019-02-13 Version 2.1.0 + * Changed internal pixel format to premultiplied 32 bit RGBA + * Added PutTile command and optimized layer initialization + * dprectool: added --msg-freq option + * Protocol change: replaced ToolChange and PenMove with DrawDabs commands + * Replaced "background layer" concept with a tiled canvas background. (Mostly compatible with MyPaint) + * Redesigned user list box and chat panel + * Autoreset threshold is now adjustable and includes reset image as base size + * Individual layers can now be censored + * Merged "Find sessions" dialog into the "Join" dialog + * Simplified "Host" dialog + * Redesigned "Login" dialog + * Moved layer hamburger menu to the main menu bar (and expanded it) + * Added support for user avatars + * Added controls for flipping, rotating and zooming to the navigator dock + * New user level: Trusted users + * Added tiered feature access controls (guest/registered/trusted/operator) + * Added zoom and rotation controls to navigator + * Removed sliders from status bar + * Added zoom tool + * Added zoom/rotation/flip toolbar + * Show keyboard shortcuts in toolbar tooltips + * Replaced recording status icon with toolbar button + * Replaced "new chat message" status icon with toolbar button (red dot indicates new message) + * Added template export feature + * Added square pixel brush + * Replaced Ffmpeg video export with native WebM export + * Added private messaging feature + 2018-08-14 Version 2.0.11 * Server: fixed OP status auto-restoration for users who used the "become operator" feature * Server AppImage: updated libmicrohttpd to version 0.9.59 diff --git a/README.md b/README.md index f8ea75e2f..77b1a5670 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Client specific dependencies: * KF5 KDNSSD (optional: local server discovery with Zeroconf) * GIFLIB (optional: animated GIF export) * MiniUPnP (optional: automatic port forwarding setup) +* LibVPX (optional: WebM video export) Server specific dependencies (you can also take a look at [Docker build](server/docker/Dockerfile) script): diff --git a/config/FindVpx.cmake b/config/FindVpx.cmake new file mode 100644 index 000000000..a30a7162f --- /dev/null +++ b/config/FindVpx.cmake @@ -0,0 +1,19 @@ +# Try to find libvpx +# +# This will define +# LIBVPX_FOUND +# LIBVPX_INCLUDE_DIRS +# LIBVPX_LIBRARIES + +find_path(LIBVPX_INCLUDE_DIR vpx_encoder.h + PATH_SUFFIXES vpx) +find_library(LIBVPX_LIBRARY NAMES vpx) + +set(LIBVPX_INCLUDE_DIRS ${LIBVPX_INCLUDE_DIR}) +set(LIBVPX_LIBRARIES ${LIBVPX_LIBRARY}) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(libvpx DEFAULT_MSG LIBVPX_LIBRARY LIBVPX_INCLUDE_DIR) + +mark_as_advanced(LIBVPX_INCLUDE_DIR LIBVPX_LIBRARY) + diff --git a/desktop/CMakeLists.txt b/desktop/CMakeLists.txt index bdc2d7793..c29e28ef2 100644 --- a/desktop/CMakeLists.txt +++ b/desktop/CMakeLists.txt @@ -20,7 +20,7 @@ endif() if( XDGMENU ) install(CODE " - execute_process(COMMAND ${XDGMENU} install --novendor ${CMAKE_CURRENT_SOURCE_DIR}/drawpile.desktop) + execute_process(COMMAND ${XDGMENU} install --novendor ${CMAKE_CURRENT_SOURCE_DIR}/net.drawpile.drawpile.desktop) ") endif() @@ -36,5 +36,5 @@ install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/palettes" DESTINATION "${DATADIR} install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/sounds" DESTINATION "${DATADIR}") install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/theme" DESTINATION "${DATADIR}") -install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/drawpile.appdata.xml" DESTINATION "${CMAKE_INSTALL_PREFIX}/share/appdata/") +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/net.drawpile.drawpile.appdata.xml" DESTINATION "${CMAKE_INSTALL_PREFIX}/share/metainfo/") diff --git a/desktop/drawpile.appdata.xml b/desktop/net.drawpile.drawpile.appdata.xml similarity index 74% rename from desktop/drawpile.appdata.xml rename to desktop/net.drawpile.drawpile.appdata.xml index 8c0eb0633..59bd28c64 100644 --- a/desktop/drawpile.appdata.xml +++ b/desktop/net.drawpile.drawpile.appdata.xml @@ -1,8 +1,12 @@ - drawpile.desktop + net.drawpile.drawpile + net.drawpile.drawpile.desktop CC0-1.0 GPL-3.0+ + Calle Laakkonen + Drawpile + Collaborative Drawing Drawpile is a drawing program that lets you share a canvas with other users in real time. Feature highlights: @@ -16,16 +20,18 @@ Discover drawing sessions via global announcement feature or locally with Zeroconf - http://drawpile.net/ + https://drawpile.net/ https://github.com/callaa/Drawpile/issues - http://drawpile.net/static/screenshots/screenshot2.png + https://drawpile.net/media/d/images/screenshot2.png + + Graphics + drawpile - Calle Laakkonen diff --git a/desktop/drawpile.desktop b/desktop/net.drawpile.drawpile.desktop similarity index 88% rename from desktop/drawpile.desktop rename to desktop/net.drawpile.drawpile.desktop index f0768e1c6..4fde069eb 100644 --- a/desktop/drawpile.desktop +++ b/desktop/net.drawpile.drawpile.desktop @@ -8,6 +8,6 @@ MimeType=image/openraster;image/png;image/jpeg;application/x-drawpile-recording; Type=Application Icon=drawpile StartupNotify=true -Categories=Graphics;Network;RasterGraphics; +Categories=Graphics; Terminal=false diff --git a/desktop/theme/dark/zoom-select.svg b/desktop/theme/dark/zoom-select.svg new file mode 100644 index 000000000..2d78c59f7 --- /dev/null +++ b/desktop/theme/dark/zoom-select.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/desktop/theme/light/zoom-select.svg b/desktop/theme/light/zoom-select.svg new file mode 100644 index 000000000..d2c3d0daa --- /dev/null +++ b/desktop/theme/light/zoom-select.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/doc/example.dptpl.txt b/doc/example.dptpl.txt deleted file mode 100644 index 6cb89429d..000000000 --- a/doc/example.dptpl.txt +++ /dev/null @@ -1,16 +0,0 @@ -########################################################################## -# -# This is a minimal session template example. -# This template creates a session with a blank 1200x1200px canvas with -# a white background layer and a transparent foreground layer. -# -########################################################################## - -# A session must begin with a "resize" command to set the initial canvas size. -1 resize right=1200 bottom=1000 - -# Create default layers -1 newlayer id=0x0101 fill=#ffffffff title=Background -1 newlayer id=0x0102 title=Foreground - - diff --git a/doc/protocol.md b/doc/protocol.md index 523b339cf..e258af4a9 100644 --- a/doc/protocol.md +++ b/doc/protocol.md @@ -99,6 +99,21 @@ Protocol stability promises: * New server features may be added at any time, but they should not break older clients, nor should a missing feature break newer clients. +### Protocol dp:4.21.2 (2.1.0) + + * Changed PutImage pixel format to ARGB32_Premultiplied + * Added PutTile command + * Removed ToolChange and PenMove commands + * Added DrawDabsClassic and DrawDabsPixel commands + * Added CanvasBackground command + * Added TrustedUsers command + * General session lock is now applied with the LayerACL command by using layer ID 0 + * Removed "lock new users by default" feature + * Replaced SessionACL message with FeatureAccessLevels message + * Added sublayer field to LayerAttributes command + * Added flags field and "censored" flag to LayerAttributes command + * Added PrivateChat transparent meta message + ### Protocol dp:4.20.1 (2.0.9) * Added `Filtered` message type. Fully backward compatible. diff --git a/pkg/mac/.gitignore b/pkg/mac/.gitignore new file mode 100644 index 000000000..f521a3ed6 --- /dev/null +++ b/pkg/mac/.gitignore @@ -0,0 +1,3 @@ +deps/ +build/ + diff --git a/pkg/mac/deps.sha256 b/pkg/mac/deps.sha256 new file mode 100644 index 000000000..7a3a29f70 --- /dev/null +++ b/pkg/mac/deps.sha256 @@ -0,0 +1,6 @@ +91b7a9359f1bfe6f667a5a9c23f6b2178555df26ca2e4dd1bb5c38dc36c77144 *extra-cmake-modules.tar.xz +34a7377ba834397db019e8eb122e551a49c98f49df75ec3fcc92b9a794a4f6d1 *giflib.tar.gz +8f28ab8a8f7236ae5e9e6cf35263dbbb87a52ec938d35515f073bc33dbc33d90 *karchive.tar.xz +677ed3d572706cc896c1e05bb2f1fb1a6c50ffaa13d1c62de13e35eca1e85803 *kdnssd.tar.xz +e8577a6acf5a168b13fc6f64d829e8ea86e917bcddf75f452bd46c69d2a6445f *libvpx.zip +e19fb5e01ea5a707e2a8cb96f537fbd9f3a913d53d804a3265e3aeab3d2064c6 *miniupnpc.tar.gz diff --git a/pkg/mac/install-deps.sh b/pkg/mac/install-deps.sh new file mode 100755 index 000000000..d32de1416 --- /dev/null +++ b/pkg/mac/install-deps.sh @@ -0,0 +1,120 @@ +#!/bin/bash + +set -e + +### VERSIONS TO DOWNLOAD +GIFLIB_URL=https://sourceforge.net/projects/giflib/files/giflib-5.1.4.tar.gz/download +MINIUPNPC_URL=http://miniupnp.free.fr/files/download.php?file=miniupnpc-2.1.tar.gz +LIBVPX_URL=https://github.com/webmproject/libvpx/archive/v1.8.0.zip +ECM_URL=https://download.kde.org/stable/frameworks/5.54/extra-cmake-modules-5.54.0.tar.xz +KARCHIVE_URL=https://download.kde.org/stable/frameworks/5.54/karchive-5.54.0.tar.xz +KDNSSD_URL=https://download.kde.org/stable/frameworks/5.54/kdnssd-5.54.0.tar.xz + +### Build flags +export CFLAGS=-mmacosx-version-min=10.7 +export CXXFLAGS=-mmacosx-version-min=10.7 + +### GENERIC FUNCTIONS +function download_package() { + URL="$1" + OUT="$2" + + if [ -f "$OUT" ] + then + echo "$OUT already downloaded. Skipping..." + else + curl -L "$URL" -o "$OUT" + fi +} + +function install_package() { + if [ -d $1-* ]; then + echo "Build directory for $1 already exists. Skipping..." + return + fi + if [ -f "$1.zip" ]; then + unzip -q "$1.zip" + elif [ -f "$1.tar.gz" ]; then + tar xfz "$1.tar.gz" + elif [ -f "$1.tar.xz" ]; then + tar xfJ "$1.tar.xz" + else + echo "BUG: Unhandled package archive format $1" + exit 1 + fi + pushd $1-* + build_$2 + popd +} + +### PACKAGE SPECIFIC BUILD SCRIPTS +function build_autoconf() { + ./configure "--prefix=$QTPATH" + make + make install +} + +function build_autoconf_libvpx() { + ./configure "--prefix=$QTPATH" --disable-vp8 --disable-vp9-decoder + make + make install +} + +function build_justmakeinstall() { + INSTALLPREFIX="$QTPATH" make install +} + +function build_cmake() { + mkdir build + cd build + cmake .. "-DCMAKE_PREFIX_PATH=$QTPATH" "-DCMAKE_INSTALL_PREFIX=$QTPATH" + make + make install +} + +### MAIN SCRIPT STARTS HERE +if [ -z "$QTPATH" ] +then + echo "QTPATH environment variable not set" + exit 1 +fi + +if [ ! -d "$QTPATH" ] +then + echo "$QTPATH is not a directory!" + exit 1 +fi + +echo "Dependencies will be downloaded to $(pwd)/deps and installed to $QTPATH." +echo "Write 'ok' to continue" + +read confirmation + +if [ "$confirmation" != "ok" ] +then + echo "Cancelled." + exit 0 +fi + +mkdir -p deps +cd deps + +# Download dependencies +download_package "$GIFLIB_URL" giflib.tar.gz +download_package "$MINIUPNPC_URL" miniupnpc.tar.gz +download_package "$LIBVPX_URL" libvpx.zip +download_package "$ECM_URL" extra-cmake-modules.tar.xz +download_package "$KARCHIVE_URL" karchive.tar.xz +download_package "$KDNSSD_URL" kdnssd.tar.xz + +# Make sure we have the right versions (and they haven't been tampered with) +shasum -a 256 -c ../deps.sha256 + +# Build and install +install_package giflib autoconf +install_package miniupnpc justmakeinstall +install_package libvpx autoconf_libvpx +install_package extra-cmake-modules cmake +install_package karchive cmake +install_package kdnssd cmake + diff --git a/pkg/mac/make-mac-dmg.sh b/pkg/mac/make-mac-dmg.sh index 70b050cac..ffb434615 100755 --- a/pkg/mac/make-mac-dmg.sh +++ b/pkg/mac/make-mac-dmg.sh @@ -1,24 +1,53 @@ #!/bin/bash -echo "NOTE: Run this script while inside of the root Drawpile directory" -echo "ANOTHER NOTE: This script is meant to be run after building drawpile, so do it after :)" -echo "Otherwise the universe might explode into popcorn..." -sleep 2 +set -e -DRAWPILE="$(pwd)" +if [ "${QTDIR+}" == "" ]; then + QTDIR="$HOME/Qt/5.9.7/clang_64" +fi + +VERSION=$(grep DRAWPILE_VERSION ../../CMakeLists.txt | cut -d \" -f 2) +TITLE="Drawpile $VERSION" + +if [ ! -d "$QTDIR" ]; then + echo "$QTDIR not found!" + exit 1 +fi + +if [ "$(which appdmg)" == "" ]; then + echo "Appdmg not found!" + echo "Run npm install -g appdmg" + exit 1 +fi + +if [ -d build ]; then + echo "Old build directory exists!" + echo "Run 'rm -rf build' and try again." + exit 1 +fi + +# Build +mkdir build +pushd build +cmake ../../../ \ + "-DCMAKE_PREFIX_PATH=$QTDIR" \ + -DSERVER=OFF \ + -DCMAKE_BUILD_TYPE=Release +make # Remove version string from the binary -cd "$DRAWPILE"/build/bin/Drawpile.app/Contents/MacOS +pushd bin +pushd Drawpile.app/Contents/MacOS BINFILE="$(readlink -n Drawpile)" rm Drawpile mv "$BINFILE" Drawpile +popd + +# Bundle frameworks +"$QTDIR/bin/macdeployqt" Drawpile.app +popd +popd # Package the app in a dmg archive -APPDMG="$(command -v appdmg)" -if [ "$APPDMG" = "/usr/local/bin/appdmg" ]; then - "$APPDMG" "$DRAWPILE"/pkg/Mac/spec.json "$DRAWPILE"/build/bin/Drawpile.dmg -else - echo "Ya done goofed, you don't have appdmg installed!; (or maybe it's not in your path, in which case sorry...)" -fi +appdmg spec.json build/bin/Drawpile.dmg -echo "The Drawpile.dmg archive is in the build directory" \ No newline at end of file diff --git a/pkg/mac/spec.json b/pkg/mac/spec.json index 9e9489f9b..a4dd98381 100644 --- a/pkg/mac/spec.json +++ b/pkg/mac/spec.json @@ -3,7 +3,7 @@ "background": "background.png", "icon": "Drawpile_Drive_Icon.icns", "contents": [ - { "x": 130, "y": 250, "type": "file", "path": "../../build/bin/Drawpile.app" }, + { "x": 130, "y": 250, "type": "file", "path": "build/bin/Drawpile.app" }, { "x": 380, "y": 250, "type": "link", "path": "/Applications" } ] } diff --git a/pkg/make-mac-pkg.sh b/pkg/make-mac-pkg.sh deleted file mode 100755 index 54343d9fe..000000000 --- a/pkg/make-mac-pkg.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -set -e - -QTDIR="$HOME/Qt/5.7/clang_64" -VERSION=$(grep DRAWPILE_VERSION ../CMakeLists.txt | cut -d \" -f 2) -TITLE="Drawpile $VERSION" - -# Create build directory -mkdir -p mac-deploy -cd mac-deploy - -# Build the app -cmake ../../ "-DCMAKE_PREFIX_PATH=$QTDIR" -DSERVER=off -DCMAKE_BUILD_TYPE=Release -make - -# Remove version string from the binary -cd bin -cd Drawpile.app/Contents/MacOS -BINFILE="$(readlink -n Drawpile)" -rm Drawpile -mv "$BINFILE" Drawpile -cd - - -"$QTDIR/bin/macdeployqt" Drawpile.app -dmg - -mv Drawpile.dmg "Drawpile $VERSION.dmg" - diff --git a/pkg/win/Dockerfile b/pkg/win/Dockerfile index 7fbd10395..8005a2e2a 100644 --- a/pkg/win/Dockerfile +++ b/pkg/win/Dockerfile @@ -1,26 +1,29 @@ FROM dockcross/windows-x64 -# Add MXE dependencies and customized settings.mk WORKDIR /usr/src/mxe -RUN git checkout master -RUN git pull + +# Get specific MXE version that is known to work (2019-01-10) +# Provides Qt 5.12.0 +RUN git fetch && git checkout 62329b26b94cf1eeb37a24f73228eda2078e44ea + +# Custom MXE settings ADD settings.mk /usr/src/mxe/ RUN make -j$(nproc) +# Download MXE deps RUN make download-qt5 -RUN make download-miniupnpc download-giflib download-libsodium +RUN make download-miniupnpc download-giflib download-libsodium download-libvpx # Patch Qt ADD qtbase-2-no-tabletevent.patch /usr/src/mxe/src/ # Build MXE dependencies -RUN make -j$(nproc) qt5 miniupnpc giflib libsodium +RUN make -j$(nproc) qt5 miniupnpc giflib libsodium libvpx # Add our own deps -ADD extra-cmake-modules.mk karchive.mk /usr/src/mxe/src/ -RUN make download-karchive download-extra-cmake-modules -ADD karchive-1-notests.patch /usr/src/mxe/src/ +ADD extra-cmake-modules.mk karchive.mk dnssd_shim.mk kdnssd.mk kdnssd-1-qtendian.patch kdnssd-2-shim.patch /usr/src/mxe/src/ +RUN make download-karchive download-extra-cmake-modules download-dnssd_shim download-kdnssd # Build our dependencies -RUN make extra-cmake-modules karchive +RUN make extra-cmake-modules karchive kdnssd diff --git a/pkg/win/dnssd_shim.mk b/pkg/win/dnssd_shim.mk new file mode 100644 index 000000000..19a7c1ea5 --- /dev/null +++ b/pkg/win/dnssd_shim.mk @@ -0,0 +1,14 @@ +PKG := dnssd_shim +$(PKG)_WEBSITE := https://github.com/callaa/dnssd_shim +$(PKG)_DESCR := dnssd.dll dynamic loader shim +$(PKG)_VERSION := 3abe0ee +$(PKG)_CHECKSUM := aaa58801ff3fcc541db06dfdede3537c9c33346bbb7a3d82912dc7069653cd32 +$(PKG)_GH_CONF := callaa/dnssd_shim/branches/master +$(PKG)_DEPS := cc + +define $(PKG)_BUILD + CC='$(PREFIX)/bin/$(TARGET)-gcc' AR='$(PREFIX)/bin/$(TARGET)-ar' $(MAKE) -C '$(1)' + cp '$(1)/libdnssd_shim.a' '$(PREFIX)/$(TARGET)/lib' + cp '$(1)/include/dns_sd.h' '$(PREFIX)/$(TARGET)/include' +endef + diff --git a/pkg/win/drawpile.iss b/pkg/win/drawpile.iss index 5d3e91805..0ccb716a0 100644 --- a/pkg/win/drawpile.iss +++ b/pkg/win/drawpile.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Drawpile" -#define MyAppVersion "2.0.11" +#define MyAppVersion "2.1.0" #define MyAppURL "http://drawpile.net/" #define MyAppExeName "drawpile.exe" diff --git a/pkg/win/extra-cmake-modules.mk b/pkg/win/extra-cmake-modules.mk index 7130c99fb..a955cb653 100644 --- a/pkg/win/extra-cmake-modules.mk +++ b/pkg/win/extra-cmake-modules.mk @@ -1,11 +1,11 @@ PKG := extra-cmake-modules $(PKG)_WEBSITE := https://community.kde.org/Frameworks $(PKG)_DESCR := KDE Frameworks 5 Extra CMake Modules -$(PKG)_VERSION := 5.41.0 -$(PKG)_CHECKSUM := baaf60940b9ff883332629ba2800090bb86722ba49a85cc12782e4ee5b169f67 +$(PKG)_VERSION := 5.55.0 +$(PKG)_CHECKSUM := 649453922aef38a24af04258ab6661ddfd566aaba4d1773a9e1f4799344406f5 $(PKG)_SUBDIR := $(PKG)-$($(PKG)_VERSION) $(PKG)_FILE := $($(PKG)_SUBDIR).tar.xz -$(PKG)_URL := http://download.kde.org/stable/frameworks/5.41/$($(PKG)_FILE) +$(PKG)_URL := http://download.kde.org/stable/frameworks/5.55/$($(PKG)_FILE) $(PKG)_DEPS := gcc qtbase define $(PKG)_BUILD diff --git a/pkg/win/karchive-1-notests.patch b/pkg/win/karchive-1-notests.patch deleted file mode 100644 index 0e284bf41..000000000 --- a/pkg/win/karchive-1-notests.patch +++ /dev/null @@ -1,12 +0,0 @@ ---- karchive-5.31.0/CMakeLists.txt.orig 2017-02-18 20:23:35.537259060 +0000 -+++ karchive-5.31.0/CMakeLists.txt 2017-02-18 20:23:39.663914927 +0000 -@@ -58,8 +58,6 @@ - SOVERSION 5) - - add_subdirectory(src) --add_subdirectory(autotests) --add_subdirectory(tests) - - - # create a Config.cmake and a ConfigVersion.cmake file and install them - diff --git a/pkg/win/karchive.mk b/pkg/win/karchive.mk index 103225bc0..c6d00e73d 100644 --- a/pkg/win/karchive.mk +++ b/pkg/win/karchive.mk @@ -1,16 +1,16 @@ PKG := karchive $(PKG)_WEBSITE := https://community.kde.org/Frameworks $(PKG)_DESCR := KDE Frameworks 5 KArchive -$(PKG)_VERSION := 5.41.0 -$(PKG)_CHECKSUM := 43c40f06e8a5e3198e5363a82748b3a7cb79526489e6bb651ca59e509297a741 +$(PKG)_VERSION := 5.55.0 +$(PKG)_CHECKSUM := 8475efa46cdc054d9fb6336e42c6075fb037921a9147d4e5aa564a5e58b79fd2 $(PKG)_SUBDIR := $(PKG)-$($(PKG)_VERSION) $(PKG)_FILE := $($(PKG)_SUBDIR).tar.xz -$(PKG)_URL := http://download.kde.org/stable/frameworks/5.41/$($(PKG)_FILE) +$(PKG)_URL := http://download.kde.org/stable/frameworks/5.55/$($(PKG)_FILE) $(PKG)_DEPS := gcc qtbase define $(PKG)_BUILD mkdir '$(1)/build' - cd '$(1)/build' && '$(TARGET)-cmake' .. -DTESTS=off + cd '$(1)/build' && '$(TARGET)-cmake' .. -DBUILD_TESTING=off $(MAKE) -C '$(1)/build' install endef diff --git a/pkg/win/kdnssd-1-qtendian.patch b/pkg/win/kdnssd-1-qtendian.patch new file mode 100644 index 000000000..891aa69fb --- /dev/null +++ b/pkg/win/kdnssd-1-qtendian.patch @@ -0,0 +1,42 @@ +diff -ru kdnssd-5.54.0.orig/src/mdnsd-publicservice.cpp kdnssd-5.54.0/src/mdnsd-publicservice.cpp +--- kdnssd-5.54.0.orig/src/mdnsd-publicservice.cpp 2019-01-20 17:23:54.407549801 +0000 ++++ kdnssd-5.54.0/src/mdnsd-publicservice.cpp 2019-01-20 17:24:50.534010980 +0000 +@@ -20,7 +20,7 @@ + + #include + #include +-#include ++#include + #include "publicservice.h" + #include "servicebase_p.h" + #include "mdnsd-sdevent.h" +@@ -172,7 +172,7 @@ + fullType += ',' + subtype; + } + if (DNSServiceRegister(&ref, 0, 0, d->m_serviceName.toUtf8().constData(), fullType.toLatin1().constData(), domainToDNS(d->m_domain).constData(), NULL, +- htons(d->m_port), TXTRecordGetLength(&txt), TXTRecordGetBytesPtr(&txt), publish_callback, ++ qToBigEndian(d->m_port), TXTRecordGetLength(&txt), TXTRecordGetBytesPtr(&txt), publish_callback, + reinterpret_cast(d)) == kDNSServiceErr_NoError) { + d->setRef(ref); + } +diff -ru kdnssd-5.54.0.orig/src/mdnsd-remoteservice.cpp kdnssd-5.54.0/src/mdnsd-remoteservice.cpp +--- kdnssd-5.54.0.orig/src/mdnsd-remoteservice.cpp 2019-01-20 17:23:54.407549801 +0000 ++++ kdnssd-5.54.0/src/mdnsd-remoteservice.cpp 2019-01-20 17:24:32.840742420 +0000 +@@ -18,7 +18,7 @@ + * Boston, MA 02110-1301, USA. + */ + +-#include ++#include + #include + #include + #include +@@ -152,7 +152,7 @@ + map[QString::fromUtf8(key)].clear(); + } + } +- ResolveEvent rev(DNSToDomain(hosttarget), ntohs(port), map); ++ ResolveEvent rev(DNSToDomain(hosttarget), qFromBigEndian(port), map); + QCoreApplication::sendEvent(obj, &rev); + } + diff --git a/pkg/win/kdnssd-2-shim.patch b/pkg/win/kdnssd-2-shim.patch new file mode 100644 index 000000000..a707495c1 --- /dev/null +++ b/pkg/win/kdnssd-2-shim.patch @@ -0,0 +1,25 @@ +diff -ru kdnssd-5.54.0.orig/cmake/FindDNSSD.cmake kdnssd-5.54.0/cmake/FindDNSSD.cmake +--- kdnssd-5.54.0.orig/cmake/FindDNSSD.cmake 2019-01-20 17:23:54.407549801 +0000 ++++ kdnssd-5.54.0/cmake/FindDNSSD.cmake 2019-01-20 17:26:15.030368312 +0000 +@@ -45,18 +45,17 @@ + if (APPLE) + set(DNSSD_LIBRARIES "/usr/lib/libSystem.dylib") + else (APPLE) +- FIND_LIBRARY(DNSSD_LIBRARIES NAMES dns_sd ) ++ FIND_LIBRARY(DNSSD_LIBRARIES NAMES dnssd_shim ) + endif (APPLE) + + cmake_push_check_state() + set(CMAKE_REQUIRED_INCLUDES ${DNSSD_INCLUDE_DIR}) + set(CMAKE_REQUIRED_LIBRARIES ${DNSSD_LIBRARIES}) +- CHECK_FUNCTION_EXISTS(DNSServiceRefDeallocate DNSSD_FUNCTION_FOUND) + cmake_pop_check_state() + +- if (DNSSD_INCLUDE_DIR AND DNSSD_LIBRARIES AND DNSSD_FUNCTION_FOUND) ++ if (DNSSD_INCLUDE_DIR AND DNSSD_LIBRARIES) + set(DNSSD_FOUND TRUE) +- endif (DNSSD_INCLUDE_DIR AND DNSSD_LIBRARIES AND DNSSD_FUNCTION_FOUND) ++ endif (DNSSD_INCLUDE_DIR AND DNSSD_LIBRARIES) + endif (DNSSD_INCLUDE_DIR) + + if (DNSSD_FOUND) diff --git a/pkg/win/kdnssd.mk b/pkg/win/kdnssd.mk new file mode 100644 index 000000000..73962b117 --- /dev/null +++ b/pkg/win/kdnssd.mk @@ -0,0 +1,16 @@ +PKG := kdnssd +$(PKG)_WEBSITE := https://community.kde.org/Frameworks +$(PKG)_DESCR := KDE Frameworks 5 KDNSSD +$(PKG)_VERSION := 5.55.0 +$(PKG)_CHECKSUM := ec9bf96ea760061d8b3a6efaf70f74de5554fe59ff8cfcdd04a6e2daeb919252 +$(PKG)_SUBDIR := $(PKG)-$($(PKG)_VERSION) +$(PKG)_FILE := $($(PKG)_SUBDIR).tar.xz +$(PKG)_URL := http://download.kde.org/stable/frameworks/5.55/$($(PKG)_FILE) +$(PKG)_DEPS := cc qtbase dnssd_shim + +define $(PKG)_BUILD + mkdir '$(1)/build' + cd '$(1)/build' && '$(TARGET)-cmake' .. -DBUILD_TESTING=off + $(MAKE) -C '$(1)/build' install +endef + diff --git a/pkg/win/make-pkg.sh b/pkg/win/make-pkg.sh index 242ca2fb2..2571d1178 100755 --- a/pkg/win/make-pkg.sh +++ b/pkg/win/make-pkg.sh @@ -29,6 +29,7 @@ cd $PKGNAME # Copy DLLs MBIN="$MXEROOT/bin" +cp "$MBIN/libwinpthread-1.dll" . cp "$MBIN/libgcc_s_seh-1.dll" . cp "$MBIN/libstdc++-6.dll" . cp "$MBIN/libstdc++-6.dll" . @@ -44,12 +45,13 @@ cp "$MBIN/libfreetype-6.dll" . cp "$MBIN/libglib-2.0-0.dll" . cp "$MBIN/libintl-8.dll" . cp "$MBIN/libiconv-2.dll" . -cp "$MBIN/libeay32.dll" . -cp "$MBIN/ssleay32.dll" . +cp "$MBIN/libcrypto-1_1-x64.dll" . +cp "$MBIN/libssl-1_1-x64.dll" . cp "$MBIN/libgif-7.dll" . cp "$MBIN/libminiupnpc.dll" . cp "$MBIN/libsqlite3-0.dll" . cp "$MBIN/libKF5Archive.dll" . +cp "$MBIN/libKF5DNSSD.dll" . cp "$MBIN/libsodium-23.dll" . QROOT="$MXEROOT/qt5" diff --git a/server/autoban.py b/server/autoban.py deleted file mode 100755 index dad64ff4f..000000000 --- a/server/autoban.py +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env python3 -"""Autoban script for Drawpile dedicated server. -This script looks through the server's configuration database and -adds serverwide bans for users who have been repeatedly banned from sessions. - -The serverwide ban duration depends on how many sessions the user was initially -banned from. - -Basic usage: autoban.py config.db -Run "autoban.py -h" for list of supported command line arguments. -""" - -import argparse -import sqlite3 -import sys -import os - -def autoban(conn, sessions, days, penalty, verbose=False): - """Scan the server log for users who have been banned from at least the - given number of different sessions in the last number of days and add - them to the serverwide banlist. - - Returns the number of new bans added. - - Arguments: - conn -- the database connection - sessions -- ban count treshold (must have been banned from this many sessions) - days -- timespan treshold (look for bans this many days in the past) - penalty -- serverwide ban will expire after this many days, multiplied per offense - verbose -- print extra info about discovered bans - - Returns: - The number of new bans added - """ - - if sessions < 1: - raise ValueError("Session count must be at least one.") - if days < 1: - raise ValueError("Must look back at least one day.") - if penalty < 1: - raise ValueError("Penalty time must be at least one day.") - - c = conn.cursor() - - # Get all in-session bans in the last days - rows = c.execute("""SELECT user, session - FROM serverlog - WHERE timestamp>=datetime('now', ?) - AND topic='Ban'""", - ['-' + str(days) + ' days']) - - # Group bans by IP address - users = {} - for row in rows: - try: - userid, userip, username = row[0].split(';', 2) - except ValueError: - print("Invalid user (ID;IP;Name) triplet: " + row[0], file=sys.stderr) - continue - - if userip not in users: - users[userip] = {'names': set(), 'sessions': set()} - users[userip]['names'].add(username) - users[userip]['sessions'].add(row[1]) - - # Gather a list of potential serverwide bans - bans = [] - for ip, info in users.items(): - if verbose: - print ('{banning}{ip} banned from {count} sessions using names "{names}"'.format( - banning="PLONK! " if (len(info['sessions']) >= sessions) else " ", - ip=ip, - names=', '.join(info['names']), - count=len(info['sessions']) - )) - - if len(info['sessions']) >= sessions: - bans.append({ - 'ip': ip, - 'expiration': '+{} days'.format(penalty * len(info['sessions'])), - 'comment': 'Banned from {count} sessions. Names used: {names}'.format( - names=', '.join(info['names']), - count=len(info['sessions']) - ) - }) - - # Filter out existing bans from the list - c.execute("SELECT ip FROM ipbans WHERE subnet=0 AND expires>datetime('now')") - active_bans = set(x[0] for x in c.fetchall()) - bans = [b for b in bans if b['ip'] not in active_bans] - - # Insert new bans - for b in bans: - c.execute("INSERT INTO ipbans VALUES (:ip, 0, datetime('now', :expiration), :comment, datetime('now'))", b) - conn.commit() - - return len(bans) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument("database", help="configuration database") - parser.add_argument("--sessions", type=int, default=3, help="number of sessions a user must be banned from") - parser.add_argument("--days", type=int, default=2, help="number of days to look back") - parser.add_argument("--penalty", type=int, default=5, help="serverwide ban duration per individual ban (days)") - parser.add_argument('--verbose', '-v', default=False, action='store_true') - args = parser.parse_args() - - if not os.path.isfile(args.database): - print("{}: file not found!".format(args.database), file=sys.stderr) - sys.exit(1) - - conn = sqlite3.connect(args.database) - try: - newbans = autoban( - conn, - sessions=args.sessions, - days=args.days, - penalty=args.penalty, - verbose=args.verbose) - - except ValueError as e: - print (str(e), file=sys.stderr) - sys.exit(1) - - finally: - conn.close() - - if newbans>0 or args.verbose: - print ("{} new ban{} added".format(newbans, 's' if newbans!=1 else '')) - diff --git a/server/bantool.py b/server/bantool.py new file mode 100755 index 000000000..f05a80359 --- /dev/null +++ b/server/bantool.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python3 +"""Ban list analysis & autoban tool for Drawpile Dedicted server. + +List bans: + bantool.py list [--expired] [--format table|json|csv] [--ip IP] + +Autoban users who have been banned often from individual sessions: + bantool.py autoban [--dry-run] [--days 2] [--sessions 3] [--penalty 5] + + The --days parameter sets the number of days to look back in the log. + The --session parameter sets the number of sessions a user must have been + banned from to earn a serverwide ban. + The --penalty parameter sets the ban duration in days per infraction. + Use the --dry-run option to see what would happen without actually banning + anyone. + +Get info about a specific IP's ban status: + bantool.py info + +Add a ban manually: + bantool.py ban + +Remove a ban manually: + bantool.py unban + + Note: autoban will restore all bans that match its criteria! + Using the special target 'expired' will delete all expired ban entries + from the database. + +Clear all active bans: + bantool.py unban all +""" + +import argparse +import itertools +import sqlite3 +import ipaddress +import json +import csv +import sys +import os + +class BadArg(Exception): + pass + +# List bans +def command_list(db, *, format, expired=False, ip=None): + """Show ban list + + Arguments: + db -- the database connection + format -- output format (table, json, csv) + expired -- show expired bans too + ip -- show only entries matching this IP + """ + + sql_filter = ["1=1"] + params = [] + + if not expired: + sql_filter.append("expires > current_timestamp") + + if ip: + sql_filter.append("in_subnet(?, ip, subnet)") + params.append(ip) + + query = "SELECT expires, added, ip, subnet, comment FROM ipbans " \ + "WHERE {filter} " \ + "ORDER BY expires DESC".format( + filter=' AND '.join(sql_filter) + ) + + rows = db.execute(query, params) + + FORMATS[format]([r[0] for r in rows.description], rows) + + +def command_autoban(db, *, sessions, days, penalty, verbose=False, dryrun=False): + """Scan the server log for users who have been banned from at least the + given number of different sessions in the last number of days and add + them to the serverwide banlist. + + Arguments: + db -- the database connection + sessions -- ban count treshold (must have been banned from this many sessions) + days -- timespan treshold (look for bans this many days in the past) + penalty -- serverwide ban will expire after this many days, multiplied per offense + verbose -- print extra info about discovered bans + dryrun -- don't actually add any new bans (setting this implies verbose=True) + """ + + if sessions < 1: + raise BadArg("Session count must be at least one.") + if days < 1: + raise BadArg("Must look back at least one day.") + if penalty < 1: + raise BadArg("Penalty time must be at least one day.") + verbose = verbose or dryrun + + # Get all in-session bans in the last days + rows = db.execute("""SELECT user, session + FROM serverlog + WHERE timestamp>=datetime('now', ?) + AND topic='Ban'""", + ['-' + str(days) + ' days'] + ) + + # Group bans by IP address + users = {} + for row in rows: + try: + userid, userip, username = row[0].split(';', 2) + except ValueError: + print("Invalid user (ID;IP;Name) triplet: " + row[0], file=sys.stderr) + continue + + if userip not in users: + users[userip] = {'names': set(), 'sessions': set()} + users[userip]['names'].add(username) + users[userip]['sessions'].add(row[1]) + + # Gather a list of potential serverwide bans + verbose_output = [] + bans = [] + for ip, info in users.items(): + if verbose: + verbose_output.append(( + ' X' if (len(info['sessions']) >= sessions) else '', + ip, + len(info['sessions']), + ', '.join(info['names']) + )) + + if len(info['sessions']) >= sessions: + bans.append({ + 'ip': ip, + 'expiration': '+{} days'.format(penalty * len(info['sessions'])), + 'comment': 'Banned from {count} sessions. Names used: {names}'.format( + names=', '.join(info['names']), + count=len(info['sessions']) + ) + }) + + # Filter out existing bans from the list + active_bans = db.execute("SELECT ip FROM ipbans WHERE subnet=0 AND expires>current_timestamp") + active_bans = set(x[0] for x in active_bans) + bans = [b for b in bans if b['ip'] not in active_bans] + + if verbose: + print_table(('Ban', 'IP', 'Sessions', 'Names used'), verbose_output) + print("---") + print("Adding %d new serverwide bans." % len(bans)) + + # Insert new bans + if not dryrun: + for b in bans: + db.execute("INSERT INTO ipbans VALUES (:ip, 0, datetime('now', :expiration), :comment, datetime('now'))", b) + db.commit() + + +def command_info(db, *, ip): + """Get ban related information about the given IP address. + """ + + + # Serverwide bans: + banned_until = db.execute( + "SELECT julianday(expires)-julianday('now'), comment FROM ipbans WHERE expires>current_timestamp AND in_subnet(?, ip, subnet)", + (ip,) + ).fetchone() + + print("IP address:", ip) + if banned_until: + print("Banned serverwide for %.1f more days" % banned_until[0]) + print("Reason:", banned_until[1]) + else: + print("No serverwide ban") + + # Session bans + rows = db.execute("""SELECT timestamp, user, session, message + FROM serverlog + WHERE topic='Ban' AND user LIKE ?""", + ['%;' + ip + ';%'] + ).fetchall() + + if rows: + print("Banned from %d sessions:" % len(rows)) + rows = map(lambda row: (row[0], row[1][row[1].rindex(';')+1:], *row[2:]), rows) + print_table(("Timestamp", "Username", "Session ID", "Message"), rows) + else: + print("Not banned from any session recently") + + +def command_ban(db, *, ip, days, message=""): + """Add a ban entry manually. + """ + if '/' in ip: + ip, subnet = ip.split('/') + subnet = int(subnet) + else: + subnet = 0 + + if subnet < 0 or subnet > 128: + raise BadArg("Invalid subnet mask") + + if days < 1: + raise BadArg("Ban duration must be at least one day") + + db.execute( + "INSERT INTO ipbans (ip, subnet, expires, comment, added) " \ + "VALUES (:ip, :subnet, datetime('now', :expiration), :msg, current_timestamp)", + { + 'ip': ip, + 'subnet': subnet, + 'expiration': '+%d days' % days, + 'msg': message, + } + ) + db.commit() + + +def command_unban(db, *, ip): + """Remove all active ban entries for the given IP""" + if ip == 'all': + c = db.execute("DELETE FROM ipbans WHERE expires > current_timestamp") + + elif ip == 'expired': + c = db.execute("DELETE FROM ipbans WHERE expires < current_timestamp") + + else: + if '/' in ip: + ip, subnet = ip.split('/') + subnet = int(subnet) + else: + subnet = 0 + + c = db.execute( + "DELETE FROM ipbans WHERE expires > current_timestamp AND ip=? AND subnet=?", + (ip, subnet) + ) + db.commit() + + print("Removed %d entries" % c.rowcount) + + +# Output formatters +def print_table(header, rows): + rows = list(rows) + columns = [0] * len(header) + + for row in itertools.chain((header,), rows): + for i, col in enumerate(row[:-1]): + columns[i] = max(columns[i], len(str(col))) + + for row in itertools.chain((header,), rows): + for width, col in zip(columns, row): + val = str(col) + sys.stdout.write(val) + sys.stdout.write(' ' * (width - len(val) + 1)) + sys.stdout.write('\n') + +def print_json(header, rows): + json.dump( + list(dict(zip(header, row)) for row in rows), + sys.stdout, + indent=0 + ) + +def print_csv(header, rows): + writer = csv.writer(sys.stdout) + writer.writerow(header) + writer.writerows(rows) + +FORMATS = { + 'table': print_table, + 'json': print_json, + 'csv': print_csv, +} + + +def _sqlite_in_subnet(ip, subnet, mask): + try: + ipaddr = ipaddress.ip_address(ip) + if mask == 0: + return ipaddr == ipaddress.ip_address(subnet) + + return ipaddr in ipaddress.ip_network(subnet + "/" + str(mask)) + except Exception as e: + print(e) + raise + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("database", + help="configuration database") + + subparsers = parser.add_subparsers(help="sub-command help") + + # List bans + cmd_list = subparsers.add_parser("list", help="List bans") + cmd_list.add_argument("--format", type=str, default='table', + help="Output format (%s)" % ', '.join(FORMATS.keys())) + cmd_list.add_argument("--expired", dest="expired", action="store_true", + help="Show expired bans") + cmd_list.add_argument("--ip", type=str, default='', + help="Show ban rules matching this IP") + cmd_list.set_defaults(func=command_list) + + # Info about a specific IPs ban status + cmd_info = subparsers.add_parser("info", help="Info about a specific IP address") + cmd_info.add_argument("ip", type=str, help="IP address") + cmd_info.set_defaults(func=command_info) + + # Autoban + cmd_autoban = subparsers.add_parser("autoban", help="Autoban repeat offenders") + cmd_autoban.add_argument("--sessions", type=int, default=3, help="number of sessions a user must be banned from") + cmd_autoban.add_argument("--days", type=int, default=2, help="number of days to look back") + cmd_autoban.add_argument("--penalty", type=int, default=5, help="serverwide ban duration per individual ban (days)") + cmd_autoban.add_argument('--verbose', '-v', default=False, action='store_true') + cmd_autoban.add_argument('--dry-run', dest="dryrun", default=False, action='store_true') + cmd_autoban.set_defaults(func=command_autoban) + + # Manually add a ban + cmd_ban = subparsers.add_parser("ban", + help="Add a ban") + cmd_ban.add_argument("ip", type=str, + help="IP address (with optional subnet)") + cmd_ban.add_argument("--days", type=int, default=365, + help="Number of days the ban lasts for (default is 1 year)") + cmd_ban.add_argument("--message", "-m", dest="message", type=str, + default="Banned manually", + help="Banlist comment") + cmd_ban.set_defaults(func=command_ban) + + # Remove a ban + cmd_unban = subparsers.add_parser("unban", + help="Remove bans") + cmd_unban.add_argument("ip", type=str, + help="IP[/subnet] | all | expired") + cmd_unban.set_defaults(func=command_unban) + + # Parse arguments + args = vars(parser.parse_args()) + + if 'func' not in args: + parser.print_help() + sys.exit(0) + + cmd = args.pop('func') + dbfile = args.pop('database') + + try: + if not os.path.isfile(dbfile): + raise BadArg(dbfile + ": file not found!") + + conn = sqlite3.connect(dbfile) + try: + conn.create_function("in_subnet", 3, _sqlite_in_subnet) + cmd(conn, **args) + finally: + conn.close() + + except BadArg as e: + print(str(e), file=sys.stderr) + sys.exit(1) + diff --git a/server/docker/Dockerfile b/server/docker/Dockerfile index 28d7e3e1f..95becd0fa 100644 --- a/server/docker/Dockerfile +++ b/server/docker/Dockerfile @@ -1,5 +1,5 @@ ## Common base -FROM alpine:3.8 as common +FROM alpine:3.9 as common RUN apk add --no-cache qt5-qtbase qt5-qtbase-sqlite libmicrohttpd libbz2 libsodium ## Build container diff --git a/server/docker/build-deps.sh b/server/docker/build-deps.sh index d7537a927..6eb1e8d03 100644 --- a/server/docker/build-deps.sh +++ b/server/docker/build-deps.sh @@ -1,5 +1,5 @@ -wget https://github.com/KDE/extra-cmake-modules/archive/v5.49.0.zip -O ecm.zip -wget https://github.com/KDE/karchive/archive/v5.49.0.zip -O karchive.zip +wget https://github.com/KDE/extra-cmake-modules/archive/v5.55.0.zip -O ecm.zip +wget https://github.com/KDE/karchive/archive/v5.55.0.zip -O karchive.zip unzip ecm.zip unzip karchive.zip diff --git a/server/logtool.py b/server/logtool.py new file mode 100755 index 000000000..fac913ce4 --- /dev/null +++ b/server/logtool.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +"""Log analysis tool for Drawpile dedicated server. + +Usage: + + logtool.py [--columns col1,col2,...] [--output ] [filters] +""" + +import argparse +import itertools +import sqlite3 +import json +import csv +import sys +import os + +COLUMNS = ("timestamp", "level", "topic", "ip", "username", "session", "message") + +class BadArg(Exception): + pass + +def logtool(db, *, columns, output_fn, ips=None, names=None, sessions=None, + topics=None, after=None, before=None): + # Select columns + for i, col in enumerate(columns): + if col not in COLUMNS: + raise BadArg("Invalid column '%s'. Must be one of (%s)" % (col, ','.join(COLUMNS))) + + if col == 'username': + columns[i] = "extract_username(user) as username" + elif col == 'ip': + columns[i] = "extract_ip(user) as ip" + + sql_filters = ["1=1"] # just so we can always have a WHERE clause + params = [] + + # Filter by IP + if ips: + sql_filters.append("extract_ip(user) IN (%s)" % (','.join('?' * len(ips)))) + params += ips + + # Filter by user name + if names: + sql_filters.append("lower(extract_username(user)) IN (%s)" % (','.join('?' * len(names)))) + params += names + + # Filter by session ID + if sessions: + sql_filters.append("session IN (%s)" % (','.join('?' * len(sessions)))) + params += sessions + + # Filter by log topic + if topics: + sql_filters.append("topic IN (%s)" % (','.join('?' * len(topics)))) + params += topics + + # Add time range filtering + if after: + sql_filters.append("datetime(timestamp) > ?") + params.append(after) + + if before: + sql_filters.append("datetime(timestamp) < ?") + params.append(before) + + # Execute query and show results + query = "SELECT {columns} FROM serverlog WHERE {where} ORDER BY timestamp".format( + columns=','.join(columns), + where=' AND '.join(sql_filters), + ) + + rows = db.execute(query, params) + output_fn([r[0] for r in rows.description], rows) + + +# Output formatters +def print_table(header, rows): + rows = list(rows) + columns = [0] * len(header) + + for row in itertools.chain((header,), rows): + for i, col in enumerate(row[:-1]): + columns[i] = max(columns[i], len(str(col))) + + for row in itertools.chain((header,), rows): + for width, col in zip(columns, row): + val = str(col) + sys.stdout.write(val) + sys.stdout.write(' ' * (width - len(val) + 1)) + sys.stdout.write('\n') + +def print_json(header, rows): + encoder = json.JSONEncoder( + ensure_ascii=False, + check_circular=False, + ) + sys.stdout.write('[') + first = True + for row in rows: + if first: + first = False + else: + sys.stdout.write(',\n') + + sys.stdout.write(encoder.encode(dict(zip(header, row)))) + sys.stdout.write(']\n') + +def print_csv(header, rows): + writer = csv.writer(sys.stdout) + writer.writerow(header) + writer.writerows(rows) + +FORMATS = { + 'table': print_table, + 'json': print_json, + 'csv': print_csv, +} + + +def _sqlite_extract_ip(value): + if not isinstance(value, str): + return '' + + idx1 = value.index(';') + if idx1 < 0: + return '' + idx2 = value.index(';', idx1+1) + + return value[idx1+1:idx2] + +def _sqlite_extract_username(value): + if not isinstance(value, str): + return '' + + idx = value.rindex(';') + if idx < 0: + return '' + return value[idx+1:] + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("database", + help="configuration database") + parser.add_argument("--columns", type=str, default='', + help="List only selected columns") + parser.add_argument("--format", type=str, default='table', + help="Output format (%s)" % ', '.join(FORMATS.keys())) + + parser.add_argument("--ip", type=str, default='', + help="Filter by IP address (comma separated list)") + parser.add_argument("--name", type=str, default='', + help="Filter by username (case insensitive comma separated list)") + parser.add_argument("--session", type=str, default='', + help="Filter by session ID (comma separated list)") + parser.add_argument("--topic", type=str, default='', + help="Filter by topic (comma separated list)") + parser.add_argument("--after", type=str, default='', + help="Show entries after this timestamp") + parser.add_argument("--before", type=str, default='', + help="Show entries before this timestamp") + + args = parser.parse_args() + + try: + if not os.path.isfile(args.database): + raise BadArg(args.database + ": file not found!") + + if args.columns: + columns = args.columns.split(',') + else: + columns = list(COLUMNS) + + if args.format not in FORMATS: + raise BadArg("Unknown output format: '%s'" % args.format) + + conn = sqlite3.connect(args.database) + try: + conn.create_function("extract_ip", 1, _sqlite_extract_ip) + conn.create_function("extract_username", 1, _sqlite_extract_username) + + logtool(conn, + columns=columns, + output_fn=FORMATS[args.format], + ips=args.ip.split(',') if args.ip else None, + names=[name.lower() for name in args.name.split(',')] if args.name else None, + sessions=args.session.split(',') if args.session else None, + topics=args.topic.split(',') if args.topic else None, + after=args.after, + before=args.before, + ) + except BrokenPipeError: + pass + finally: + conn.close() + + except BadArg as e: + print(str(e), file=sys.stderr) + sys.exit(1) + diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index 7fc41b443..f93b37608 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -1,6 +1,7 @@ find_package(Qt5Gui REQUIRED) find_package(Qt5Svg REQUIRED) find_package(Qt5LinguistTools) +find_package(Vpx) set ( SOURCES @@ -9,7 +10,7 @@ set ( tools/toolproperties.cpp tools/utils.cpp tools/annotation.cpp - tools/brushes.cpp + tools/freehand.cpp tools/colorpicker.cpp tools/laser.cpp tools/selection.cpp @@ -17,6 +18,7 @@ set ( tools/beziertool.cpp tools/floodfill.cpp tools/strokesmoother.cpp + tools/zoom.cpp canvas/statetracker.cpp canvas/canvasmodel.cpp canvas/selection.cpp @@ -57,16 +59,25 @@ set ( utils/icon.cpp utils/logging.cpp utils/passwordstore.cpp + utils/identicon.cpp + utils/avatarlistmodel.cpp + utils/sessionfilterproxymodel.cpp core/annotationmodel.cpp core/tile.cpp core/layer.cpp core/layerstack.cpp - core/brush.cpp core/brushmask.cpp core/blendmodes.cpp core/rasterop.cpp - core/shapes.cpp core/floodfill.cpp + core/tilevector.cpp + brushes/brushengine.cpp + brushes/brushpainter.cpp + brushes/classicbrushstate.cpp + brushes/classicbrushpainter.cpp + brushes/pixelbrushstate.cpp + brushes/pixelbrushpainter.cpp + brushes/shapes.cpp ora/orawriter.cpp ora/orareader.cpp recording/index.cpp @@ -77,10 +88,11 @@ set ( export/animation.cpp export/videoexporter.cpp export/imageseriesexporter.cpp - export/ffmpegexporter.cpp parentalcontrols/parentalcontrols.cpp ) +include_directories(bundled) + if(WIN32) set(SOURCES ${SOURCES} parentalcontrols/parentalcontrols_win.cpp) else() @@ -106,6 +118,16 @@ if(KF5DNSSD_FOUND) add_definitions(-DHAVE_DNSSD) endif() +if(LIBVPX_FOUND) + set( + SOURCES ${SOURCES} + bundled/mkvmuxer/mkvmuxer.cc + bundled/mkvmuxer/mkvmuxerutil.cc + export/webmexporter.cpp + export/webmencoder.cpp + ) +endif(LIBVPX_FOUND) + if( Qt5LinguistTools_FOUND) set(TRANSLATIONS i18n/drawpile_fi.ts @@ -143,6 +165,10 @@ if(LIBMINIUPNPC_FOUND) endif ( WIN32 ) endif() +if(LIBVPX_FOUND) + target_link_libraries(${DPCLIENTLIB} ${LIBVPX_LIBRARIES}) +endif(LIBVPX_FOUND) + if ( WIN32 ) target_link_libraries (${DPCLIENTLIB} ws2_32) endif () diff --git a/src/client/brushes/brush.h b/src/client/brushes/brush.h new file mode 100644 index 000000000..d4b7de40f --- /dev/null +++ b/src/client/brushes/brush.h @@ -0,0 +1,148 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2006-2019 Calle Laakkonen + + Drawpile is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Drawpile is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Drawpile. If not, see . +*/ +#ifndef DP_BRUSHES_BRUSH_H +#define DP_BRUSHES_BRUSH_H + +#include "../core/blendmodes.h" + +#include + +namespace brushes { + +/** + * @brief The parameters for Drawpile's classic soft and pixel brushes + * + * Pressure sensitive brush parameters come in pairs: x1 and x2, + * where 1 is the value at full pressure and 2 is the value at zero pressure. + * + * Pressure sensitive brush parameters are: + * + * - Size - the brush diameter + * - Hardness - hardness of the brush shape + * - Opacity - brush opacity + * - Smudge - smudging factor (how much color is picked up) + * + * Constant parameters are: + * + * - Resmudge - how often to sample smudge color (every resmudge dabs) + * - Spacing - dab spacing as a percentage of brush diameter + * - Subpixel mode (boolean) + * - Incremental mode (boolean) + * - Blending mode + * - Shape selection (round/square) + */ +class ClassicBrush +{ +public: + //! Construct a brush + ClassicBrush(int size=1, qreal hardness=1.0, qreal opacity=1.0, + const QColor& color=Qt::black, int spacing=25) + : m_size1(size), m_size2(size), + m_hardness1(hardness), m_hardness2(hardness), + m_opacity1(opacity), m_opacity2(opacity), + m_smudge1(0), m_smudge2(0), + m_color(color), m_spacing(spacing), m_resmudge(0), + m_blend(paintcore::BlendMode::MODE_NORMAL), + m_subpixel(false), m_incremental(true), + m_square(false) + { + } + + void setSize(int size) { Q_ASSERT(size>0); m_size1 = size; } + void setSize2(int size) { Q_ASSERT(size>0); m_size2 = size; } + + int size1() const { return m_size1; } + int size2() const { return m_size2; } + + void setHardness(qreal hardness) { Q_ASSERT(hardness>=0 && hardness<=1); m_hardness1 = hardness; } + void setHardness2(qreal hardness) { Q_ASSERT(hardness>=0 && hardness<=1); m_hardness2 = hardness; } + + qreal hardness1() const { return m_hardness1; } + qreal hardness2() const { return m_hardness2; } + + void setOpacity(qreal opacity) { Q_ASSERT(opacity>=0 && opacity<=1); m_opacity1 = opacity; } + void setOpacity2(qreal opacity) { Q_ASSERT(opacity>=0 && opacity<=1); m_opacity2 = opacity; } + + qreal opacity1() const { return m_opacity1; } + qreal opacity2() const { return m_opacity2; } + + void setColor(const QColor& color) { m_color = color; } + const QColor &color() const { return m_color; } + + void setSmudge(qreal smudge) { Q_ASSERT(smudge>=0 && smudge<=1); m_smudge1 = smudge; } + void setSmudge2(qreal smudge) { Q_ASSERT(smudge>=0 && smudge<=1); m_smudge2 = smudge; } + + qreal smudge1() const { return m_smudge1; } + qreal smudge2() const { return m_smudge2; } + + void setSpacing(int spacing) { Q_ASSERT(spacing >= 0 && spacing <= 100); m_spacing = spacing; } + int spacing() const { return m_spacing; } + + //! Set smudge colir resampling frequency (0 resamples on every dab) + void setResmudge(int resmudge) { Q_ASSERT(resmudge >= 0); m_resmudge = resmudge; } + int resmudge() const { return m_resmudge; } + + void setSubpixel(bool sp) { m_subpixel = sp; } + bool subpixel() const { return m_subpixel; } + + void setIncremental(bool incremental) { m_incremental = incremental; } + bool incremental() const { return m_incremental; } + + void setBlendingMode(paintcore::BlendMode::Mode mode) { m_blend = mode; } + paintcore::BlendMode::Mode blendingMode() const { return m_blend; } + bool isEraser() const { return m_blend == paintcore::BlendMode::MODE_ERASE; } + + void setSquare(bool square) { m_square = square; } + bool isSquare() const { return m_square; } + + qreal size(qreal pressure) const { return lerp(size1(), size2(), pressure); } + qreal hardness(qreal pressure) const { return lerp(hardness1(), hardness2(), pressure); } + qreal opacity(qreal pressure) const { return lerp(opacity1(), opacity2(), pressure); } + qreal smudge(qreal pressure) const { return lerp(smudge1(), smudge2(), pressure); } + qreal spacingDist(qreal pressure) const { return spacing() / 100.0 * size(pressure); } + + //! Does opacity vary with pressure? + bool isOpacityVariable() const { return qAbs(opacity1() - opacity2()) > (1/256.0); } + +private: + static inline qreal lerp(qreal a, qreal b, qreal alpha) { + Q_ASSERT(alpha >=0 && alpha<=1); + return (a-b) * alpha + b; + } + + int m_size1, m_size2; + qreal m_hardness1, m_hardness2; + qreal m_opacity1, m_opacity2; + qreal m_smudge1, m_smudge2; + QColor m_color; + int m_spacing; + int m_resmudge; + paintcore::BlendMode::Mode m_blend; + bool m_subpixel; + bool m_incremental; + bool m_square; +}; + +} + +Q_DECLARE_TYPEINFO(brushes::ClassicBrush, Q_MOVABLE_TYPE); + +#endif + + diff --git a/src/client/brushes/brushengine.cpp b/src/client/brushes/brushengine.cpp new file mode 100644 index 000000000..7ba743f22 --- /dev/null +++ b/src/client/brushes/brushengine.cpp @@ -0,0 +1,48 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018 Calle Laakkonen + + Drawpile is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Drawpile is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Drawpile. If not, see . +*/ + +#include "brushengine.h" + +#include "classicbrushstate.h" +#include "pixelbrushstate.h" + +namespace brushes { + +BrushEngine::BrushEngine() +{ +} + +void BrushEngine::setBrush(int contextId, int layerId, const ClassicBrush &brush) +{ + // Select brush engine to use + if(brush.subpixel()) { + m_activeEngine = &m_classic; + m_classic.setBrush(brush); + m_classic.setContextId(contextId); + m_classic.setLayer(layerId); + + } else { + m_activeEngine = &m_pixel; + m_pixel.setBrush(brush); + m_pixel.setContextId(contextId); + m_pixel.setLayer(layerId); + } +} + +} diff --git a/src/client/brushes/brushengine.h b/src/client/brushes/brushengine.h new file mode 100644 index 000000000..45101cca8 --- /dev/null +++ b/src/client/brushes/brushengine.h @@ -0,0 +1,53 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018 Calle Laakkonen + + Drawpile is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Drawpile is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Drawpile. If not, see . +*/ + +#ifndef DP_BRUSHENGINE_H +#define DP_BRUSHENGINE_H + +#include "brushstate.h" +#include "classicbrushstate.h" +#include "pixelbrushstate.h" +#include "../shared/net/message.h" + +namespace brushes { + +/** + * @brief An abstraction layer for brush engines + */ +class BrushEngine : public BrushState +{ +public: + BrushEngine(); + + void setBrush(int contextId, int layerId, const ClassicBrush &brush); + + void strokeTo(const paintcore::Point &p, const paintcore::Layer *sourceLayer) override { m_activeEngine->strokeTo(p, sourceLayer); } + void endStroke() override { m_activeEngine->endStroke(); } + QList takeDabs() override { return m_activeEngine->takeDabs(); } + +private: + BrushState *m_activeEngine; + + ClassicBrushState m_classic; + PixelBrushState m_pixel; +}; + +} + +#endif diff --git a/src/client/brushes/brushpainter.cpp b/src/client/brushes/brushpainter.cpp new file mode 100644 index 000000000..18301e74f --- /dev/null +++ b/src/client/brushes/brushpainter.cpp @@ -0,0 +1,68 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018-2019 Calle Laakkonen + + Drawpile is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Drawpile is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Drawpile. If not, see . +*/ + +#include "brushpainter.h" +#include "classicbrushpainter.h" +#include "pixelbrushpainter.h" + +#include "core/layerstack.h" +#include "core/layer.h" +#include "../shared/net/brushes.h" + +namespace brushes { + +void drawBrushDabs(const protocol::Message &msg, paintcore::EditableLayerStack &layers) +{ + auto layer = layers.getEditableLayer(msg.layer()); + if(layer.isNull()) { + qWarning("drawBrushDabs(ctx=%d, layer=%d): no such layer", msg.contextId(), msg.layer()); + return; + } + + drawBrushDabsDirect(msg, layer); +} + +/** + * @brief Draw brush dabs onto a specific layer + * + * Typically, you should call drawBrushDabs instead. However, this function + * must be called directly when you're drawing onto a preview sublayer. + * + * @param msg brush dab message + * @param layer the layer to draw onto + */ +void drawBrushDabsDirect(const protocol::Message &msg, paintcore::EditableLayer layer, int sublayer) +{ + Q_ASSERT(!layer.isNull()); + + switch(msg.type()) { + case protocol::MSG_DRAWDABS_CLASSIC: + drawClassicBrushDabs(static_cast(msg), layer, sublayer); + break; + case protocol::MSG_DRAWDABS_PIXEL: + case protocol::MSG_DRAWDABS_PIXEL_SQUARE: + drawPixelBrushDabs(static_cast(msg), layer, sublayer); + break; + default: + qWarning("Unhandled dab type: %s", qPrintable(msg.messageName())); + } +} + +} + diff --git a/src/client/brushes/brushpainter.h b/src/client/brushes/brushpainter.h new file mode 100644 index 000000000..2414b8f10 --- /dev/null +++ b/src/client/brushes/brushpainter.h @@ -0,0 +1,58 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018-2019 Calle Laakkonen + + Drawpile is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Drawpile is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Drawpile. If not, see . +*/ + +#ifndef BRUSHES_BRUSHPAINTER_H +#define BRUSHES_BRUSHPAINTER_H + +namespace protocol { + class Message; +} + +namespace paintcore { + class EditableLayerStack; + class EditableLayer; +} + +namespace brushes { + +/** + * @brief Draw brush dabs onto the canvas + * + * The layer is selected automatically + * + * @param msg brush dab message + * @param layers layer stack + */ +void drawBrushDabs(const protocol::Message &msg, paintcore::EditableLayerStack &layers); + +/** + * @brief Draw brush dabs onto a specific layer + * + * Typically, you should call drawBrushDabs instead. However, this function + * must be called directly when you're drawing onto a preview sublayer. + * + * @param msg brush dab message + * @param layer the layer to draw onto + * @param sublayer sublayer override + */ +void drawBrushDabsDirect(const protocol::Message &msg, paintcore::EditableLayer layer, int sublayer=0); + +} + +#endif diff --git a/src/client/brushes/brushstate.h b/src/client/brushes/brushstate.h new file mode 100644 index 000000000..a26b994d2 --- /dev/null +++ b/src/client/brushes/brushstate.h @@ -0,0 +1,62 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018 Calle Laakkonen + + Drawpile is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Drawpile is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Drawpile. If not, see . +*/ + +#ifndef DP_BRUSHSTATE_H +#define DP_BRUSHSTATE_H + +#include "../shared/net/message.h" + +namespace paintcore { + class Point; + class Layer; +} + +namespace brushes { + +/** + * @brief An abstract base class for brush engines + */ +class BrushState { +public: + virtual ~BrushState() { } + + /** + * @brief Start or continue a stroke + * @param sourceLayer layer to pick up color from (when smudging.) May be nullptr + */ + virtual void strokeTo(const paintcore::Point &p, const paintcore::Layer *sourceLayer) = 0; + + /** + * @brief End the active stroke + */ + virtual void endStroke() = 0; + + /** + * @brief Take the list of DrawDab* commands accumulated so far. + * + * This clears the dab buffer but does not end the stroke. + * + * @return + */ + virtual QList takeDabs() = 0; +}; + +} + +#endif diff --git a/src/client/brushes/classicbrushpainter.cpp b/src/client/brushes/classicbrushpainter.cpp new file mode 100644 index 000000000..5447d7aad --- /dev/null +++ b/src/client/brushes/classicbrushpainter.cpp @@ -0,0 +1,297 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2013-2019 Calle Laakkonen + + Drawpile is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Drawpile is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Drawpile. If not, see . +*/ + +#include "../shared/net/brushes.h" +#include "core/brushmask.h" +#include "core/layer.h" + +#include + +#include + +namespace brushes { + +namespace { + +template T square(T x) { return x*x; } + +typedef QVector LUT; +static const int LUT_RADIUS = 128; +static QCache LUT_CACHE; + +// Generate a lookup table for Gimp style exponential brush shape +// The value at r² (where r is distance from brush center, scaled to LUT_RADIUS) is +// the opaqueness of the pixel. +static LUT makeGimpStyleBrushLUT(float hardness) +{ + qreal exponent; + if ((1.0 - hardness) < 0.0000004) + exponent = 1000000.0; + else + exponent = 0.4 / (1.0 - hardness); + + LUT lut(square(LUT_RADIUS)); + for(int i=0;i=0 && h<=100); + if(!LUT_CACHE.contains(h)) + LUT_CACHE.insert(h, new LUT(makeGimpStyleBrushLUT(hardness))); + + return *LUT_CACHE[h]; +} + +static paintcore::BrushStamp makeMask(qreal r, qreal hardness, qreal opacity) +{ + r /= 2.0; + opacity = opacity * 255; + + // generate mask + QVector data; + int diameter; + int stampOffset; + + if(r<1) { + // special case for single pixel brush + diameter=3; + stampOffset = -1; + data.resize(3*3); + data.fill(0); + data[4] = opacity; + + } else { + const LUT lut = cachedGimpStyleBrushLUT(hardness); + const float lut_scale = square((LUT_RADIUS-1) / r); + + float offset; + float fudge=1; + diameter = ceil(r*2) + 2; + + if(diameter%2==0) { + ++diameter; + offset = -1.0; + + if(r<8) + fudge = 0.9; + } else { + offset = -0.5; + } + stampOffset = -diameter/2; + + // empirically determined fudge factors to make small brushes look nice + if(r<2.5) + fudge=0.8; + + else if(r<4) + fudge=0.8; + + data.resize(square(diameter)); + uchar *ptr = data.data(); + + for(int y=0;y data(square(diameter)); + uchar *ptr = data.data(); + + for(int y=0;y1 || yfrac<0 || yfrac>1) + qWarning("offsetMask(mask, %f, %f): offset out of bounds!", xfrac, yfrac); +#endif + + const int diameter = mask.diameter(); + + const qreal kernel[] = { + xfrac*yfrac, + (1.0-xfrac)*yfrac, + xfrac*(1.0-yfrac), + (1.0-xfrac)*(1.0-yfrac) + }; +#ifndef NDEBUG + const qreal kernelsum = fabs(kernel[0]+kernel[1]+kernel[2]+kernel[3]-1.0); + if(kernelsum>0.001) + qWarning("offset kernel sum error=%f", kernelsum); +#endif + + const uchar *src = mask.data(); + + QVector data(square(diameter)); + uchar *ptr = data.data(); + +#if 0 + for(int y=-1;y0) + sublayer = dabs.contextId(); + + if(sublayer != 0) { + layer = layer.getEditableSubLayer(sublayer, blendmode, color.alpha() > 0 ? color.alpha() : 255); + layer.updateChangeBounds(dabs.bounds()); + blendmode = paintcore::BlendMode::MODE_NORMAL; + } + + int lastX = dabs.originX(); + int lastY = dabs.originY(); + for(const protocol::ClassicBrushDab &d : dabs.dabs()) { + const int nextX = lastX + d.x; + const int nextY = lastY + d.y; + const paintcore::BrushStamp bs = makeGimpStyleBrushStamp( + QPointF(nextX/4.0, nextY/4.0), + d.size/256.0, + d.hardness/255.0, + d.opacity/255.0 + ); + layer.putBrushStamp(bs, color, blendmode); + lastX = nextX; + lastY = nextY; + } +} + +} diff --git a/src/client/brushes/classicbrushpainter.h b/src/client/brushes/classicbrushpainter.h new file mode 100644 index 000000000..b3bd17528 --- /dev/null +++ b/src/client/brushes/classicbrushpainter.h @@ -0,0 +1,46 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018-2019 Calle Laakkonen + + Drawpile is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Drawpile is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Drawpile. If not, see . +*/ +#ifndef BRUSHES_CLASSICBRUSHPAINTER_H +#define BRUSHES_CLASSICBRUSHPAINTER_H + +#include + +class QPointF; + +namespace paintcore { + class EditableLayer; + struct BrushStamp; +} + +namespace protocol { + class DrawDabsClassic; +} + +namespace brushes { + +/** + * Draw brush drabs on the canvas + */ +void drawClassicBrushDabs(const protocol::DrawDabsClassic &dabs, paintcore::EditableLayer layer, int sublayer=0); + +paintcore::BrushStamp makeGimpStyleBrushStamp(const QPointF &point, qreal radius, qreal hardness, qreal opacity); + +} + +#endif diff --git a/src/client/brushes/classicbrushstate.cpp b/src/client/brushes/classicbrushstate.cpp new file mode 100644 index 000000000..f87e3e8fd --- /dev/null +++ b/src/client/brushes/classicbrushstate.cpp @@ -0,0 +1,160 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018-2019 Calle Laakkonen + + Drawpile is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Drawpile is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Drawpile. If not, see . +*/ + +#include "classicbrushstate.h" +#include "core/layerstack.h" +#include "core/brushmask.h" +#include "core/layer.h" + +namespace brushes { + +ClassicBrushState::ClassicBrushState() + : m_contextId(0), m_layerId(0), + m_length(0), m_smudgeDistance(0), m_pendown(false), + m_lastDab(nullptr), m_lastDabX(0), m_lastDabY(0) +{ +} + +void ClassicBrushState::setBrush(const ClassicBrush &brush) +{ + m_brush = brush; + + QColor c = m_brush.color(); + if(brush.incremental()) { + c.setAlpha(0); + + } else { + // If brush alpha is nonzero, indirect drawing mode + // is used and the alpha is used as the overall transparency + // of the entire stroke. + c.setAlphaF(m_brush.opacity1()); + + m_brush.setOpacity(1.0); + m_brush.setOpacity2(brush.isOpacityVariable() ? 0.0 : 1.0); + } + m_brush.setColor(c); + m_smudgedColor = c; + + if(m_pendown) + qWarning("Brush changed mid-stroke!"); +} + +void ClassicBrushState::strokeTo(const paintcore::Point &to, const paintcore::Layer *sourceLayer) +{ + if(m_pendown) { + // Stroke in progress: draw a line + qreal dx = to.x() - m_lastPoint.x(); + qreal dy = to.y() - m_lastPoint.y(); + const qreal dist = hypot(dx, dy); + dx = dx / dist; + dy = dy / dist; + const qreal dp = (to.pressure() - m_lastPoint.pressure()) / dist; + + const qreal spacing0 = qMax(1.0, m_brush.spacingDist(m_lastPoint.pressure())); + qreal i; + if(m_length>=spacing0) + i = 0; + else if(m_length==0) + i = spacing0; + else + i = m_length; + + paintcore::Point p(m_lastPoint.x() + dx*i, m_lastPoint.y() + dy*i, qBound(0.0, m_lastPoint.pressure() + dp*i, 1.0)); + + while(i<=dist) { + const qreal spacing = qMax(1.0, m_brush.spacingDist(p.pressure())); + const qreal smudge = m_brush.smudge(p.pressure()); + + if(++m_smudgeDistance > m_brush.resmudge() && smudge>0 && sourceLayer) { + const QColor sampled = sourceLayer->colorAt(p.x(), p.y(), qRound(m_brush.size(p.pressure()))); + + const qreal a = sampled.alphaF() * smudge; + + m_smudgedColor = QColor::fromRgbF( + m_smudgedColor.redF() * (1-a) + sampled.redF() * a, + m_smudgedColor.greenF() * (1-a) + sampled.greenF() * a, + m_smudgedColor.blueF() * (1-a) + sampled.blueF() * a, + 0 + ); + m_smudgeDistance = 0; + } + + addDab(p, m_smudgedColor.rgba()); + + p.rx() += dx * spacing; + p.ry() += dy * spacing; + p.setPressure(qBound(0.0, p.pressure() + dp * spacing, 1.0)); + i += spacing; + } + m_length = i-dist; + + } else { + // Start a new stroke + m_pendown = true; + addDab(to, m_smudgedColor.rgba()); + } + + m_lastPoint = to; +} + +void ClassicBrushState::addDab(const paintcore::Point &point, quint32 color) +{ + const int x = point.x() * 4; + const int y = point.y() * 4; + + if(!m_lastDab + || m_lastDab->color() != color + || qAbs(x - m_lastDabX) > protocol::ClassicBrushDab::MAX_XY_DELTA + || qAbs(y - m_lastDabY) > protocol::ClassicBrushDab::MAX_XY_DELTA + || m_lastDab->dabs().size() >= protocol::DrawDabsClassic::MAX_DABS + ) { + m_lastDab = new protocol::DrawDabsClassic( + m_contextId, + m_layerId, + x, + y, + color, + m_brush.blendingMode() + ); + m_dabs << protocol::MessagePtr(m_lastDab); + m_lastDabX = x; + m_lastDabY = y; + } + + m_lastDab->dabs() << protocol::ClassicBrushDab { + static_cast(x - m_lastDabX), + static_cast(y - m_lastDabY), + static_cast(m_brush.size(point.pressure()) * 256), + static_cast(m_brush.hardness(point.pressure()) * 255), + static_cast(m_brush.opacity(point.pressure()) * 255) + }; + + m_lastDabX = x; + m_lastDabY = y; +} + +void ClassicBrushState::endStroke() +{ + m_pendown = false; + m_length = 0; + m_smudgeDistance = 0; + m_smudgedColor = m_brush.color(); +} + +} diff --git a/src/client/brushes/classicbrushstate.h b/src/client/brushes/classicbrushstate.h new file mode 100644 index 000000000..8543c58ea --- /dev/null +++ b/src/client/brushes/classicbrushstate.h @@ -0,0 +1,111 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018 Calle Laakkonen + + Drawpile is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Drawpile is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Drawpile. If not, see . +*/ +#ifndef BRUSHES_BRUSHSTATE_H +#define BRUSHES_BRUSHSTATE_H + +#include "brush.h" +#include "brushstate.h" +#include "core/point.h" +#include "../shared/net/brushes.h" + +namespace paintcore { + class LayerStack; + class Layer; + struct BrushStamp; +} + +namespace brushes { + +/** + * @brief Drawpile's classic brush engine + * + * This class keeps track of a brush stroke's state and generates + * DrawDabs commands. + */ +class ClassicBrushState : public BrushState { +public: + ClassicBrushState(); + + /** + * @brief Set the context (user) ID + * @param id + */ + void setContextId(int id) { m_contextId = id; } + + /** + * @brief Set the brush parameters + */ + void setBrush(const brushes::ClassicBrush &brush); + + /** + * @brief Set the target layer + * @param id layer ID + */ + void setLayer(int id) { m_layerId = id; } + + /** + * @brief Start or continue a stroke + * @param sourceLayer layer to pick up color from (when smudging) + */ + void strokeTo(const paintcore::Point &p, const paintcore::Layer *sourceLayer) override; + + /** + * @brief End the active stroke + */ + void endStroke() override; + + /** + * @brief Take the current DrawDab commands + * + * This clears the dab buffer but does not end the + * stroke. + * + * @return list of DrawDab commands generated so far + */ + QList takeDabs() override { + auto dabs = m_dabs; + m_dabs = QList(); + m_lastDab = nullptr; + return dabs; + } + +private: + void addDab(const paintcore::Point &point, quint32 color); + + ClassicBrush m_brush; // the current brush + int m_contextId; // user context ID + int m_layerId; // target layer ID + qreal m_length; // current length of active brush stroke + int m_smudgeDistance; // dabs since last smudge color sampling + QColor m_smudgedColor; // effective color (nonzero alpha indicates indirect drawing mode) + bool m_pendown; // brush stroke in progress? + paintcore::Point m_lastPoint; + + QList m_dabs; + protocol::DrawDabsClassic *m_lastDab; + int m_lastDabX; + int m_lastDabY; +}; + +} + +#endif + + + diff --git a/src/client/brushes/pixelbrushpainter.cpp b/src/client/brushes/pixelbrushpainter.cpp new file mode 100644 index 000000000..52f07683b --- /dev/null +++ b/src/client/brushes/pixelbrushpainter.cpp @@ -0,0 +1,101 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2013-2019 Calle Laakkonen + + Drawpile is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Drawpile is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Drawpile. If not, see . +*/ + +#include "../shared/net/brushes.h" +#include "core/brushmask.h" +#include "core/layer.h" + +namespace brushes { + +template static T square(T x) { return x*x; } + +static paintcore::BrushMask makeRoundPixelBrushMask(int diameter, uchar opacity) +{ + const qreal radius = diameter/2.0; + const qreal rr = square(radius); + + QVector data(square(diameter), 0); + uchar *ptr = data.data(); + + const qreal offset = 0.5; + for(int y=0;y(square(diameter), opacity)); +} + +void drawPixelBrushDabs(const protocol::DrawDabsPixel &dabs, paintcore::EditableLayer layer, int sublayer) +{ + if(dabs.dabs().isEmpty()) { + qWarning("drawPixelBrushDabs(ctx=%d, layer=%d): empty dab vector!", dabs.contextId(), dabs.layer()); + return; + } + + auto blendmode = paintcore::BlendMode::Mode(dabs.mode()); + const QColor color = QColor::fromRgba(dabs.color()); + + if(sublayer == 0 && color.alpha()>0) + sublayer = dabs.contextId(); + + if(sublayer != 0) { + layer = layer.getEditableSubLayer(sublayer, blendmode, color.alpha() > 0 ? color.alpha() : 255); + layer.updateChangeBounds(dabs.bounds()); + blendmode = paintcore::BlendMode::MODE_NORMAL; + } + + paintcore::BrushMask mask; + int lastSize = -1, lastOpacity = 0; + + int lastX = dabs.originX(); + int lastY = dabs.originY(); + for(const protocol::PixelBrushDab &d : dabs.dabs()) { + const int nextX = lastX + d.x; + const int nextY = lastY + d.y; + + if(d.size != lastSize|| d.opacity != lastOpacity) { + // The mask is often reusable + mask = dabs.isSquare() ? makeSquarePixelBrushMask(d.size, d.opacity) : makeRoundPixelBrushMask(d.size, d.opacity); + lastSize = d.size; + lastOpacity = d.opacity; + } + + const int offset = d.size/2; + layer.putBrushStamp( + paintcore::BrushStamp { nextX-offset, nextY-offset, mask }, + color, + blendmode + ); + + lastX = nextX; + lastY = nextY; + } +} + +} diff --git a/src/client/brushes/pixelbrushpainter.h b/src/client/brushes/pixelbrushpainter.h new file mode 100644 index 000000000..fc41627fc --- /dev/null +++ b/src/client/brushes/pixelbrushpainter.h @@ -0,0 +1,39 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018-2019 Calle Laakkonen + + Drawpile is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Drawpile is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Drawpile. If not, see . +*/ +#ifndef BRUSHES_PIXELBRUSHPAINTER_H +#define BRUSHES_PIXELBRUSHPAINTER_H + +namespace paintcore { + class Layer; +} + +namespace protocol { + class DrawDabsPixel; +} + +namespace brushes { + +/** + * Draw brush drabs on the canvas + */ +void drawPixelBrushDabs(const protocol::DrawDabsPixel &dabs, paintcore::EditableLayer layer, int sublayer=0); + +} + +#endif diff --git a/src/client/brushes/pixelbrushstate.cpp b/src/client/brushes/pixelbrushstate.cpp new file mode 100644 index 000000000..768044a7b --- /dev/null +++ b/src/client/brushes/pixelbrushstate.cpp @@ -0,0 +1,176 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018-2019 Calle Laakkonen + + Drawpile is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Drawpile is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Drawpile. If not, see . +*/ + +#include "pixelbrushstate.h" +#include "core/layerstack.h" +#include "core/brushmask.h" +#include "core/layer.h" + +#include + +namespace brushes { + +PixelBrushState::PixelBrushState() + : m_contextId(0), m_layerId(0), + m_length(0), m_pendown(false), + m_lastDab(nullptr), m_lastDabX(0), m_lastDabY(0) +{ +} + +void PixelBrushState::setBrush(const ClassicBrush &brush) +{ + m_brush = brush; + + QColor c = m_brush.color(); + if(brush.incremental()) { + c.setAlpha(0); + + } else { + // If brush alpha is nonzero, indirect drawing mode + // is used and the alpha is used as the overall transparency + // of the entire stroke. + c.setAlphaF(m_brush.opacity1()); + + m_brush.setOpacity(1.0); + m_brush.setOpacity2(brush.isOpacityVariable() ? 0.0 : 1.0); + } + m_brush.setColor(c); + + if(m_pendown) + qWarning("Brush changed mid-stroke!"); +} + +void PixelBrushState::strokeTo(const paintcore::Point &to, const paintcore::Layer *) +{ + if(m_pendown) { + // Stroke in progress: draw a line + + const qreal dp = (to.pressure()-m_lastPoint.pressure()) / hypot(to.x()-m_lastPoint.x(), to.y()-m_lastPoint.y()); + + int x0 = qFloor(m_lastPoint.x()); + int y0 = qFloor(m_lastPoint.y()); + qreal p = m_lastPoint.pressure(); + int x1 = qFloor(to.x()); + int y1 = qFloor(to.y()); + int dy = y1 - y0; + int dx = x1 - x0; + int stepx, stepy; + + if (dy < 0) { + dy = -dy; + stepy = -1; + } else { + stepy = 1; + } + if (dx < 0) { + dx = -dx; + stepx = -1; + } else { + stepx = 1; + } + + dy *= 2; + dx *= 2; + + qreal distance = m_length; + + if (dx > dy) { + int fraction = dy - (dx >> 1); + while (x0 != x1) { + const qreal spacing = m_brush.spacingDist(p); + if (fraction >= 0) { + y0 += stepy; + fraction -= dx; + } + x0 += stepx; + fraction += dy; + if(++distance >= spacing) { + addDab(x0, y0, p); + distance = 0; + } + p += dp; + } + } else { + int fraction = dx - (dy >> 1); + while (y0 != y1) { + const qreal spacing = m_brush.spacingDist(p); + if (fraction >= 0) { + x0 += stepx; + fraction -= dy; + } + y0 += stepy; + fraction += dx; + if(++distance >= spacing) { + addDab(x0, y0, p); + distance = 0; + } + p += dp; + } + } + + m_length = distance; + + } else { + // Start a new stroke + m_pendown = true; + addDab(to.x(), to.y(), to.pressure()); + } + + m_lastPoint = to; +} + +void PixelBrushState::addDab(int x, int y, qreal pressure) +{ + if(!m_lastDab + || qAbs(x - m_lastDabX) > protocol::PixelBrushDab::MAX_XY_DELTA + || qAbs(y - m_lastDabY) > protocol::PixelBrushDab::MAX_XY_DELTA + || m_lastDab->dabs().size() >= protocol::DrawDabsPixel::MAX_DABS + ) { + m_lastDab = new protocol::DrawDabsPixel( + m_brush.isSquare() ? protocol::DabShape::Square : protocol::DabShape::Round, + m_contextId, + m_layerId, + x, + y, + m_brush.color().rgba(), + m_brush.blendingMode() + ); + m_dabs << protocol::MessagePtr(m_lastDab); + m_lastDabX = x; + m_lastDabY = y; + } + + m_lastDab->dabs() << protocol::PixelBrushDab { + static_cast(x - m_lastDabX), + static_cast(y - m_lastDabY), + static_cast(m_brush.size(pressure)), + static_cast(m_brush.opacity(pressure) * 255) + }; + + m_lastDabX = x; + m_lastDabY = y; +} + +void PixelBrushState::endStroke() +{ + m_pendown = false; + m_length = 0; +} + +} diff --git a/src/client/brushes/pixelbrushstate.h b/src/client/brushes/pixelbrushstate.h new file mode 100644 index 000000000..b41341812 --- /dev/null +++ b/src/client/brushes/pixelbrushstate.h @@ -0,0 +1,106 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018 Calle Laakkonen + + Drawpile is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Drawpile is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Drawpile. If not, see . +*/ +#ifndef BRUSHES_PIXELBRUSHSTATE_H +#define BRUSHES_PIXELBRUSHSTATE_H + +#include "brush.h" +#include "brushstate.h" +#include "core/point.h" +#include "../shared/net/brushes.h" + +namespace paintcore { + class LayerStack; + class Layer; + struct BrushStamp; +} + +namespace brushes { + +/** + * @brief Pixel brush engine + * + * This class keeps track of a brush stroke's state and generates + * DrawDabs commands. + */ +class PixelBrushState : public BrushState { +public: + PixelBrushState(); + + /** + * @brief Set the context (user) ID + * @param id + */ + void setContextId(int id) { m_contextId = id; } + + /** + * @brief Set the brush parameters + */ + void setBrush(const ClassicBrush &brush); + + /** + * @brief Set the target layer + * @param id layer ID + */ + void setLayer(int id) { m_layerId = id; } + + /** + * @brief Start or continue a stroke + * @param sourceLayer unused + */ + void strokeTo(const paintcore::Point &p, const paintcore::Layer *) override; + + /** + * @brief End the active stroke + */ + void endStroke() override; + + /** + * @brief Take the current DrawDab commands + * + * This clears the dab buffer but does not end the + * stroke. + * + * @return list of DrawDab commands generated so far + */ + QList takeDabs() override { + auto dabs = m_dabs; + m_dabs = QList(); + m_lastDab = nullptr; + return dabs; + } + +private: + void addDab(int x, int y, qreal pressure); + + ClassicBrush m_brush; // the current brush + int m_contextId; // user context ID + int m_layerId; // target layer ID + qreal m_length; // current length of active brush stroke + bool m_pendown; // brush stroke in progress? + paintcore::Point m_lastPoint; + + QList m_dabs; + protocol::DrawDabsPixel *m_lastDab; + int m_lastDabX; + int m_lastDabY; +}; + +} + +#endif diff --git a/src/client/core/shapes.cpp b/src/client/brushes/shapes.cpp similarity index 93% rename from src/client/core/shapes.cpp rename to src/client/brushes/shapes.cpp index 3555d3113..a5e840227 100644 --- a/src/client/core/shapes.cpp +++ b/src/client/brushes/shapes.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2014 Calle Laakkonen + Copyright (C) 2014-2018 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,13 +19,15 @@ #include "shapes.h" -#include -#include +#include #include -namespace paintcore{ +namespace brushes { namespace shapes { +using paintcore::Point; +using paintcore::PointVector; + PointVector rectangle(const QRectF &rect) { const QPointF p1 = rect.topLeft(); @@ -53,7 +55,7 @@ PointVector ellipse(const QRectF &rect) // TODO smart step size selection for(qreal t=0;t<2*M_PI;t+=M_PI/20) { - pv << Point(cx + a*qCos(t), cy + b*qSin(t), 1.0); + pv << Point(cx + a*cos(t), cy + b*sin(t), 1.0); } pv << Point(cx+a, cy, 1); return pv; @@ -102,7 +104,7 @@ PointVector sampleStroke(const QRectF &rect) const qreal fx = x/qreal(strokew); const qreal pressure = qBound(0.0, ((fx*fx) - (fx*fx*fx))*6.756, 1.0); - const qreal y = qSin(phase) * strokeh; + const qreal y = sin(phase) * strokeh; pointvector << Point(rect.left()+x, offy+y, pressure); } return pointvector; diff --git a/src/client/core/shapes.h b/src/client/brushes/shapes.h similarity index 64% rename from src/client/core/shapes.h rename to src/client/brushes/shapes.h index 707b22fec..3750ca963 100644 --- a/src/client/core/shapes.h +++ b/src/client/brushes/shapes.h @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2014 Calle Laakkonen + Copyright (C) 2014-2018 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,23 +17,24 @@ along with Drawpile. If not, see . */ -#ifndef DP_PAINTCORE_SHAPES_H -#define DP_PAINTCORE_SHAPES_H +#ifndef DP_BRUSHES_SHAPES_H +#define DP_BRUSHES_SHAPES_H + +#include "core/point.h" #include -#include "point.h" -namespace paintcore { +namespace brushes { namespace shapes { -PointVector rectangle(const QRectF &rect); -PointVector ellipse(const QRectF &rect); +paintcore::PointVector rectangle(const QRectF &rect); +paintcore::PointVector ellipse(const QRectF &rect); -PointVector cubicBezierCurve(const QPointF p[4]); +paintcore::PointVector cubicBezierCurve(const QPointF p[4]); // These are used for brush previews -PointVector sampleStroke(const QRectF &rect); -PointVector sampleBlob(const QRectF &rect); +paintcore::PointVector sampleStroke(const QRectF &rect); +paintcore::PointVector sampleBlob(const QRectF &rect); } } diff --git a/src/client/bundled/LICENSE.mkvmuxer b/src/client/bundled/LICENSE.mkvmuxer new file mode 100644 index 000000000..7a6f99547 --- /dev/null +++ b/src/client/bundled/LICENSE.mkvmuxer @@ -0,0 +1,30 @@ +Copyright (c) 2010, Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * 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. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +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/src/client/bundled/README b/src/client/bundled/README new file mode 100644 index 000000000..ec4139e81 --- /dev/null +++ b/src/client/bundled/README @@ -0,0 +1,11 @@ +# mkvmuxer + +Extracted from libwebm version 1.0.0.27 +If a prepackaged libwebm is available, it can be substituted for this bundled version. + +Modifications: + + - Removed chunking (unused feature, removes dependency on mkvparser and MkvWriter) + - Removed Segmet::CopyAndMoveCuesBeforeClusters (unused feature, removed dependency on mkvparser) + - Removed unneeded `#include "mkvwriter.h"` from mkvmuxerutil.cc + diff --git a/src/client/bundled/mkvmuxer/common/webmids.h b/src/client/bundled/mkvmuxer/common/webmids.h new file mode 100644 index 000000000..32a0c5fb9 --- /dev/null +++ b/src/client/bundled/mkvmuxer/common/webmids.h @@ -0,0 +1,184 @@ +// Copyright (c) 2012 The WebM project authors. All Rights Reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. An additional intellectual property rights grant can be found +// in the file PATENTS. All contributing project authors may +// be found in the AUTHORS file in the root of the source tree. + +#ifndef COMMON_WEBMIDS_H_ +#define COMMON_WEBMIDS_H_ + +namespace libwebm { + +enum MkvId { + kMkvEBML = 0x1A45DFA3, + kMkvEBMLVersion = 0x4286, + kMkvEBMLReadVersion = 0x42F7, + kMkvEBMLMaxIDLength = 0x42F2, + kMkvEBMLMaxSizeLength = 0x42F3, + kMkvDocType = 0x4282, + kMkvDocTypeVersion = 0x4287, + kMkvDocTypeReadVersion = 0x4285, + kMkvVoid = 0xEC, + kMkvSignatureSlot = 0x1B538667, + kMkvSignatureAlgo = 0x7E8A, + kMkvSignatureHash = 0x7E9A, + kMkvSignaturePublicKey = 0x7EA5, + kMkvSignature = 0x7EB5, + kMkvSignatureElements = 0x7E5B, + kMkvSignatureElementList = 0x7E7B, + kMkvSignedElement = 0x6532, + // segment + kMkvSegment = 0x18538067, + // Meta Seek Information + kMkvSeekHead = 0x114D9B74, + kMkvSeek = 0x4DBB, + kMkvSeekID = 0x53AB, + kMkvSeekPosition = 0x53AC, + // Segment Information + kMkvInfo = 0x1549A966, + kMkvTimecodeScale = 0x2AD7B1, + kMkvDuration = 0x4489, + kMkvDateUTC = 0x4461, + kMkvTitle = 0x7BA9, + kMkvMuxingApp = 0x4D80, + kMkvWritingApp = 0x5741, + // Cluster + kMkvCluster = 0x1F43B675, + kMkvTimecode = 0xE7, + kMkvPrevSize = 0xAB, + kMkvBlockGroup = 0xA0, + kMkvBlock = 0xA1, + kMkvBlockDuration = 0x9B, + kMkvReferenceBlock = 0xFB, + kMkvLaceNumber = 0xCC, + kMkvSimpleBlock = 0xA3, + kMkvBlockAdditions = 0x75A1, + kMkvBlockMore = 0xA6, + kMkvBlockAddID = 0xEE, + kMkvBlockAdditional = 0xA5, + kMkvDiscardPadding = 0x75A2, + // Track + kMkvTracks = 0x1654AE6B, + kMkvTrackEntry = 0xAE, + kMkvTrackNumber = 0xD7, + kMkvTrackUID = 0x73C5, + kMkvTrackType = 0x83, + kMkvFlagEnabled = 0xB9, + kMkvFlagDefault = 0x88, + kMkvFlagForced = 0x55AA, + kMkvFlagLacing = 0x9C, + kMkvDefaultDuration = 0x23E383, + kMkvMaxBlockAdditionID = 0x55EE, + kMkvName = 0x536E, + kMkvLanguage = 0x22B59C, + kMkvCodecID = 0x86, + kMkvCodecPrivate = 0x63A2, + kMkvCodecName = 0x258688, + kMkvCodecDelay = 0x56AA, + kMkvSeekPreRoll = 0x56BB, + // video + kMkvVideo = 0xE0, + kMkvFlagInterlaced = 0x9A, + kMkvStereoMode = 0x53B8, + kMkvAlphaMode = 0x53C0, + kMkvPixelWidth = 0xB0, + kMkvPixelHeight = 0xBA, + kMkvPixelCropBottom = 0x54AA, + kMkvPixelCropTop = 0x54BB, + kMkvPixelCropLeft = 0x54CC, + kMkvPixelCropRight = 0x54DD, + kMkvDisplayWidth = 0x54B0, + kMkvDisplayHeight = 0x54BA, + kMkvDisplayUnit = 0x54B2, + kMkvAspectRatioType = 0x54B3, + kMkvFrameRate = 0x2383E3, + // end video + // colour + kMkvColour = 0x55B0, + kMkvMatrixCoefficients = 0x55B1, + kMkvBitsPerChannel = 0x55B2, + kMkvChromaSubsamplingHorz = 0x55B3, + kMkvChromaSubsamplingVert = 0x55B4, + kMkvCbSubsamplingHorz = 0x55B5, + kMkvCbSubsamplingVert = 0x55B6, + kMkvChromaSitingHorz = 0x55B7, + kMkvChromaSitingVert = 0x55B8, + kMkvRange = 0x55B9, + kMkvTransferCharacteristics = 0x55BA, + kMkvPrimaries = 0x55BB, + kMkvMaxCLL = 0x55BC, + kMkvMaxFALL = 0x55BD, + // mastering metadata + kMkvMasteringMetadata = 0x55D0, + kMkvPrimaryRChromaticityX = 0x55D1, + kMkvPrimaryRChromaticityY = 0x55D2, + kMkvPrimaryGChromaticityX = 0x55D3, + kMkvPrimaryGChromaticityY = 0x55D4, + kMkvPrimaryBChromaticityX = 0x55D5, + kMkvPrimaryBChromaticityY = 0x55D6, + kMkvWhitePointChromaticityX = 0x55D7, + kMkvWhitePointChromaticityY = 0x55D8, + kMkvLuminanceMax = 0x55D9, + kMkvLuminanceMin = 0x55DA, + // end mastering metadata + // end colour + // audio + kMkvAudio = 0xE1, + kMkvSamplingFrequency = 0xB5, + kMkvOutputSamplingFrequency = 0x78B5, + kMkvChannels = 0x9F, + kMkvBitDepth = 0x6264, + // end audio + // ContentEncodings + kMkvContentEncodings = 0x6D80, + kMkvContentEncoding = 0x6240, + kMkvContentEncodingOrder = 0x5031, + kMkvContentEncodingScope = 0x5032, + kMkvContentEncodingType = 0x5033, + kMkvContentCompression = 0x5034, + kMkvContentCompAlgo = 0x4254, + kMkvContentCompSettings = 0x4255, + kMkvContentEncryption = 0x5035, + kMkvContentEncAlgo = 0x47E1, + kMkvContentEncKeyID = 0x47E2, + kMkvContentSignature = 0x47E3, + kMkvContentSigKeyID = 0x47E4, + kMkvContentSigAlgo = 0x47E5, + kMkvContentSigHashAlgo = 0x47E6, + kMkvContentEncAESSettings = 0x47E7, + kMkvAESSettingsCipherMode = 0x47E8, + kMkvAESSettingsCipherInitData = 0x47E9, + // end ContentEncodings + // Cueing Data + kMkvCues = 0x1C53BB6B, + kMkvCuePoint = 0xBB, + kMkvCueTime = 0xB3, + kMkvCueTrackPositions = 0xB7, + kMkvCueTrack = 0xF7, + kMkvCueClusterPosition = 0xF1, + kMkvCueBlockNumber = 0x5378, + // Chapters + kMkvChapters = 0x1043A770, + kMkvEditionEntry = 0x45B9, + kMkvChapterAtom = 0xB6, + kMkvChapterUID = 0x73C4, + kMkvChapterStringUID = 0x5654, + kMkvChapterTimeStart = 0x91, + kMkvChapterTimeEnd = 0x92, + kMkvChapterDisplay = 0x80, + kMkvChapString = 0x85, + kMkvChapLanguage = 0x437C, + kMkvChapCountry = 0x437E, + // Tags + kMkvTags = 0x1254C367, + kMkvTag = 0x7373, + kMkvSimpleTag = 0x67C8, + kMkvTagName = 0x45A3, + kMkvTagString = 0x4487 +}; + +} // namespace libwebm + +#endif // COMMON_WEBMIDS_H_ diff --git a/src/client/bundled/mkvmuxer/mkvmuxer.cc b/src/client/bundled/mkvmuxer/mkvmuxer.cc new file mode 100644 index 000000000..06c890727 --- /dev/null +++ b/src/client/bundled/mkvmuxer/mkvmuxer.cc @@ -0,0 +1,3682 @@ +// Copyright (c) 2012 The WebM project authors. All Rights Reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. An additional intellectual property rights grant can be found +// in the file PATENTS. All contributing project authors may +// be found in the AUTHORS file in the root of the source tree. + +#include "mkvmuxer/mkvmuxer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/webmids.h" +#include "mkvmuxer/mkvmuxerutil.h" + +namespace mkvmuxer { + +const float MasteringMetadata::kValueNotPresent = FLT_MAX; +const uint64_t Colour::kValueNotPresent = UINT64_MAX; + +namespace { + +const char kDocTypeWebm[] = "webm"; +const char kDocTypeMatroska[] = "matroska"; + +// Deallocate the string designated by |dst|, and then copy the |src| +// string to |dst|. The caller owns both the |src| string and the +// |dst| copy (hence the caller is responsible for eventually +// deallocating the strings, either directly, or indirectly via +// StrCpy). Returns true if the source string was successfully copied +// to the destination. +bool StrCpy(const char* src, char** dst_ptr) { + if (dst_ptr == NULL) + return false; + + char*& dst = *dst_ptr; + + delete[] dst; + dst = NULL; + + if (src == NULL) + return true; + + const size_t size = strlen(src) + 1; + + dst = new (std::nothrow) char[size]; // NOLINT + if (dst == NULL) + return false; + + strcpy(dst, src); // NOLINT + return true; +} + +typedef std::auto_ptr PrimaryChromaticityPtr; +bool CopyChromaticity(const PrimaryChromaticity* src, + PrimaryChromaticityPtr* dst) { + if (!dst) + return false; + + dst->reset(new (std::nothrow) PrimaryChromaticity(src->x, src->y)); + if (!dst->get()) + return false; + + return true; +} + +} // namespace + +/////////////////////////////////////////////////////////////// +// +// IMkvWriter Class + +IMkvWriter::IMkvWriter() {} + +IMkvWriter::~IMkvWriter() {} + +bool WriteEbmlHeader(IMkvWriter* writer, uint64_t doc_type_version, + const char* const doc_type) { + // Level 0 + uint64_t size = + EbmlElementSize(libwebm::kMkvEBMLVersion, static_cast(1)); + size += EbmlElementSize(libwebm::kMkvEBMLReadVersion, static_cast(1)); + size += EbmlElementSize(libwebm::kMkvEBMLMaxIDLength, static_cast(4)); + size += + EbmlElementSize(libwebm::kMkvEBMLMaxSizeLength, static_cast(8)); + size += EbmlElementSize(libwebm::kMkvDocType, doc_type); + size += EbmlElementSize(libwebm::kMkvDocTypeVersion, + static_cast(doc_type_version)); + size += + EbmlElementSize(libwebm::kMkvDocTypeReadVersion, static_cast(2)); + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvEBML, size)) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvEBMLVersion, + static_cast(1))) { + return false; + } + if (!WriteEbmlElement(writer, libwebm::kMkvEBMLReadVersion, + static_cast(1))) { + return false; + } + if (!WriteEbmlElement(writer, libwebm::kMkvEBMLMaxIDLength, + static_cast(4))) { + return false; + } + if (!WriteEbmlElement(writer, libwebm::kMkvEBMLMaxSizeLength, + static_cast(8))) { + return false; + } + if (!WriteEbmlElement(writer, libwebm::kMkvDocType, doc_type)) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvDocTypeVersion, + static_cast(doc_type_version))) { + return false; + } + if (!WriteEbmlElement(writer, libwebm::kMkvDocTypeReadVersion, + static_cast(2))) { + return false; + } + + return true; +} + +bool WriteEbmlHeader(IMkvWriter* writer, uint64_t doc_type_version) { + return WriteEbmlHeader(writer, doc_type_version, kDocTypeWebm); +} + +bool WriteEbmlHeader(IMkvWriter* writer) { + return WriteEbmlHeader(writer, mkvmuxer::Segment::kDefaultDocTypeVersion); +} + +/////////////////////////////////////////////////////////////// +// +// Frame Class + +Frame::Frame() + : add_id_(0), + additional_(NULL), + additional_length_(0), + duration_(0), + duration_set_(false), + frame_(NULL), + is_key_(false), + length_(0), + track_number_(0), + timestamp_(0), + discard_padding_(0), + reference_block_timestamp_(0), + reference_block_timestamp_set_(false) {} + +Frame::~Frame() { + delete[] frame_; + delete[] additional_; +} + +bool Frame::CopyFrom(const Frame& frame) { + delete[] frame_; + frame_ = NULL; + length_ = 0; + if (frame.length() > 0 && frame.frame() != NULL && + !Init(frame.frame(), frame.length())) { + return false; + } + add_id_ = 0; + delete[] additional_; + additional_ = NULL; + additional_length_ = 0; + if (frame.additional_length() > 0 && frame.additional() != NULL && + !AddAdditionalData(frame.additional(), frame.additional_length(), + frame.add_id())) { + return false; + } + duration_ = frame.duration(); + duration_set_ = frame.duration_set(); + is_key_ = frame.is_key(); + track_number_ = frame.track_number(); + timestamp_ = frame.timestamp(); + discard_padding_ = frame.discard_padding(); + reference_block_timestamp_ = frame.reference_block_timestamp(); + reference_block_timestamp_set_ = frame.reference_block_timestamp_set(); + return true; +} + +bool Frame::Init(const uint8_t* frame, uint64_t length) { + uint8_t* const data = + new (std::nothrow) uint8_t[static_cast(length)]; // NOLINT + if (!data) + return false; + + delete[] frame_; + frame_ = data; + length_ = length; + + memcpy(frame_, frame, static_cast(length_)); + return true; +} + +bool Frame::AddAdditionalData(const uint8_t* additional, uint64_t length, + uint64_t add_id) { + uint8_t* const data = + new (std::nothrow) uint8_t[static_cast(length)]; // NOLINT + if (!data) + return false; + + delete[] additional_; + additional_ = data; + additional_length_ = length; + add_id_ = add_id; + + memcpy(additional_, additional, static_cast(additional_length_)); + return true; +} + +bool Frame::IsValid() const { + if (length_ == 0 || !frame_) { + return false; + } + if ((additional_length_ != 0 && !additional_) || + (additional_ != NULL && additional_length_ == 0)) { + return false; + } + if (track_number_ == 0 || track_number_ > kMaxTrackNumber) { + return false; + } + if (!CanBeSimpleBlock() && !is_key_ && !reference_block_timestamp_set_) { + return false; + } + return true; +} + +bool Frame::CanBeSimpleBlock() const { + return additional_ == NULL && discard_padding_ == 0 && duration_ == 0; +} + +void Frame::set_duration(uint64_t duration) { + duration_ = duration; + duration_set_ = true; +} + +void Frame::set_reference_block_timestamp(int64_t reference_block_timestamp) { + reference_block_timestamp_ = reference_block_timestamp; + reference_block_timestamp_set_ = true; +} + +/////////////////////////////////////////////////////////////// +// +// CuePoint Class + +CuePoint::CuePoint() + : time_(0), + track_(0), + cluster_pos_(0), + block_number_(1), + output_block_number_(true) {} + +CuePoint::~CuePoint() {} + +bool CuePoint::Write(IMkvWriter* writer) const { + if (!writer || track_ < 1 || cluster_pos_ < 1) + return false; + + uint64_t size = EbmlElementSize(libwebm::kMkvCueClusterPosition, + static_cast(cluster_pos_)); + size += EbmlElementSize(libwebm::kMkvCueTrack, static_cast(track_)); + if (output_block_number_ && block_number_ > 1) + size += EbmlElementSize(libwebm::kMkvCueBlockNumber, + static_cast(block_number_)); + const uint64_t track_pos_size = + EbmlMasterElementSize(libwebm::kMkvCueTrackPositions, size) + size; + const uint64_t payload_size = + EbmlElementSize(libwebm::kMkvCueTime, static_cast(time_)) + + track_pos_size; + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvCuePoint, payload_size)) + return false; + + const int64_t payload_position = writer->Position(); + if (payload_position < 0) + return false; + + if (!WriteEbmlElement(writer, libwebm::kMkvCueTime, + static_cast(time_))) { + return false; + } + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvCueTrackPositions, size)) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvCueTrack, + static_cast(track_))) { + return false; + } + if (!WriteEbmlElement(writer, libwebm::kMkvCueClusterPosition, + static_cast(cluster_pos_))) { + return false; + } + if (output_block_number_ && block_number_ > 1) { + if (!WriteEbmlElement(writer, libwebm::kMkvCueBlockNumber, + static_cast(block_number_))) { + return false; + } + } + + const int64_t stop_position = writer->Position(); + if (stop_position < 0) + return false; + + if (stop_position - payload_position != static_cast(payload_size)) + return false; + + return true; +} + +uint64_t CuePoint::PayloadSize() const { + uint64_t size = EbmlElementSize(libwebm::kMkvCueClusterPosition, + static_cast(cluster_pos_)); + size += EbmlElementSize(libwebm::kMkvCueTrack, static_cast(track_)); + if (output_block_number_ && block_number_ > 1) + size += EbmlElementSize(libwebm::kMkvCueBlockNumber, + static_cast(block_number_)); + const uint64_t track_pos_size = + EbmlMasterElementSize(libwebm::kMkvCueTrackPositions, size) + size; + const uint64_t payload_size = + EbmlElementSize(libwebm::kMkvCueTime, static_cast(time_)) + + track_pos_size; + + return payload_size; +} + +uint64_t CuePoint::Size() const { + const uint64_t payload_size = PayloadSize(); + return EbmlMasterElementSize(libwebm::kMkvCuePoint, payload_size) + + payload_size; +} + +/////////////////////////////////////////////////////////////// +// +// Cues Class + +Cues::Cues() + : cue_entries_capacity_(0), + cue_entries_size_(0), + cue_entries_(NULL), + output_block_number_(true) {} + +Cues::~Cues() { + if (cue_entries_) { + for (int32_t i = 0; i < cue_entries_size_; ++i) { + CuePoint* const cue = cue_entries_[i]; + delete cue; + } + delete[] cue_entries_; + } +} + +bool Cues::AddCue(CuePoint* cue) { + if (!cue) + return false; + + if ((cue_entries_size_ + 1) > cue_entries_capacity_) { + // Add more CuePoints. + const int32_t new_capacity = + (!cue_entries_capacity_) ? 2 : cue_entries_capacity_ * 2; + + if (new_capacity < 1) + return false; + + CuePoint** const cues = + new (std::nothrow) CuePoint*[new_capacity]; // NOLINT + if (!cues) + return false; + + for (int32_t i = 0; i < cue_entries_size_; ++i) { + cues[i] = cue_entries_[i]; + } + + delete[] cue_entries_; + + cue_entries_ = cues; + cue_entries_capacity_ = new_capacity; + } + + cue->set_output_block_number(output_block_number_); + cue_entries_[cue_entries_size_++] = cue; + return true; +} + +CuePoint* Cues::GetCueByIndex(int32_t index) const { + if (cue_entries_ == NULL) + return NULL; + + if (index >= cue_entries_size_) + return NULL; + + return cue_entries_[index]; +} + +uint64_t Cues::Size() { + uint64_t size = 0; + for (int32_t i = 0; i < cue_entries_size_; ++i) + size += GetCueByIndex(i)->Size(); + size += EbmlMasterElementSize(libwebm::kMkvCues, size); + return size; +} + +bool Cues::Write(IMkvWriter* writer) const { + if (!writer) + return false; + + uint64_t size = 0; + for (int32_t i = 0; i < cue_entries_size_; ++i) { + const CuePoint* const cue = GetCueByIndex(i); + + if (!cue) + return false; + + size += cue->Size(); + } + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvCues, size)) + return false; + + const int64_t payload_position = writer->Position(); + if (payload_position < 0) + return false; + + for (int32_t i = 0; i < cue_entries_size_; ++i) { + const CuePoint* const cue = GetCueByIndex(i); + + if (!cue->Write(writer)) + return false; + } + + const int64_t stop_position = writer->Position(); + if (stop_position < 0) + return false; + + if (stop_position - payload_position != static_cast(size)) + return false; + + return true; +} + +/////////////////////////////////////////////////////////////// +// +// ContentEncAESSettings Class + +ContentEncAESSettings::ContentEncAESSettings() : cipher_mode_(kCTR) {} + +uint64_t ContentEncAESSettings::Size() const { + const uint64_t payload = PayloadSize(); + const uint64_t size = + EbmlMasterElementSize(libwebm::kMkvContentEncAESSettings, payload) + + payload; + return size; +} + +bool ContentEncAESSettings::Write(IMkvWriter* writer) const { + const uint64_t payload = PayloadSize(); + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvContentEncAESSettings, + payload)) + return false; + + const int64_t payload_position = writer->Position(); + if (payload_position < 0) + return false; + + if (!WriteEbmlElement(writer, libwebm::kMkvAESSettingsCipherMode, + static_cast(cipher_mode_))) { + return false; + } + + const int64_t stop_position = writer->Position(); + if (stop_position < 0 || + stop_position - payload_position != static_cast(payload)) + return false; + + return true; +} + +uint64_t ContentEncAESSettings::PayloadSize() const { + uint64_t size = EbmlElementSize(libwebm::kMkvAESSettingsCipherMode, + static_cast(cipher_mode_)); + return size; +} + +/////////////////////////////////////////////////////////////// +// +// ContentEncoding Class + +ContentEncoding::ContentEncoding() + : enc_algo_(5), + enc_key_id_(NULL), + encoding_order_(0), + encoding_scope_(1), + encoding_type_(1), + enc_key_id_length_(0) {} + +ContentEncoding::~ContentEncoding() { delete[] enc_key_id_; } + +bool ContentEncoding::SetEncryptionID(const uint8_t* id, uint64_t length) { + if (!id || length < 1) + return false; + + delete[] enc_key_id_; + + enc_key_id_ = + new (std::nothrow) uint8_t[static_cast(length)]; // NOLINT + if (!enc_key_id_) + return false; + + memcpy(enc_key_id_, id, static_cast(length)); + enc_key_id_length_ = length; + + return true; +} + +uint64_t ContentEncoding::Size() const { + const uint64_t encryption_size = EncryptionSize(); + const uint64_t encoding_size = EncodingSize(0, encryption_size); + const uint64_t encodings_size = + EbmlMasterElementSize(libwebm::kMkvContentEncoding, encoding_size) + + encoding_size; + + return encodings_size; +} + +bool ContentEncoding::Write(IMkvWriter* writer) const { + const uint64_t encryption_size = EncryptionSize(); + const uint64_t encoding_size = EncodingSize(0, encryption_size); + const uint64_t size = + EbmlMasterElementSize(libwebm::kMkvContentEncoding, encoding_size) + + encoding_size; + + const int64_t payload_position = writer->Position(); + if (payload_position < 0) + return false; + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvContentEncoding, + encoding_size)) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvContentEncodingOrder, + static_cast(encoding_order_))) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvContentEncodingScope, + static_cast(encoding_scope_))) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvContentEncodingType, + static_cast(encoding_type_))) + return false; + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvContentEncryption, + encryption_size)) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvContentEncAlgo, + static_cast(enc_algo_))) { + return false; + } + if (!WriteEbmlElement(writer, libwebm::kMkvContentEncKeyID, enc_key_id_, + enc_key_id_length_)) + return false; + + if (!enc_aes_settings_.Write(writer)) + return false; + + const int64_t stop_position = writer->Position(); + if (stop_position < 0 || + stop_position - payload_position != static_cast(size)) + return false; + + return true; +} + +uint64_t ContentEncoding::EncodingSize(uint64_t compresion_size, + uint64_t encryption_size) const { + // TODO(fgalligan): Add support for compression settings. + if (compresion_size != 0) + return 0; + + uint64_t encoding_size = 0; + + if (encryption_size > 0) { + encoding_size += + EbmlMasterElementSize(libwebm::kMkvContentEncryption, encryption_size) + + encryption_size; + } + encoding_size += EbmlElementSize(libwebm::kMkvContentEncodingType, + static_cast(encoding_type_)); + encoding_size += EbmlElementSize(libwebm::kMkvContentEncodingScope, + static_cast(encoding_scope_)); + encoding_size += EbmlElementSize(libwebm::kMkvContentEncodingOrder, + static_cast(encoding_order_)); + + return encoding_size; +} + +uint64_t ContentEncoding::EncryptionSize() const { + const uint64_t aes_size = enc_aes_settings_.Size(); + + uint64_t encryption_size = EbmlElementSize(libwebm::kMkvContentEncKeyID, + enc_key_id_, enc_key_id_length_); + encryption_size += EbmlElementSize(libwebm::kMkvContentEncAlgo, + static_cast(enc_algo_)); + + return encryption_size + aes_size; +} + +/////////////////////////////////////////////////////////////// +// +// Track Class + +Track::Track(unsigned int* seed) + : codec_id_(NULL), + codec_private_(NULL), + language_(NULL), + max_block_additional_id_(0), + name_(NULL), + number_(0), + type_(0), + uid_(MakeUID(seed)), + codec_delay_(0), + seek_pre_roll_(0), + default_duration_(0), + codec_private_length_(0), + content_encoding_entries_(NULL), + content_encoding_entries_size_(0) {} + +Track::~Track() { + delete[] codec_id_; + delete[] codec_private_; + delete[] language_; + delete[] name_; + + if (content_encoding_entries_) { + for (uint32_t i = 0; i < content_encoding_entries_size_; ++i) { + ContentEncoding* const encoding = content_encoding_entries_[i]; + delete encoding; + } + delete[] content_encoding_entries_; + } +} + +bool Track::AddContentEncoding() { + const uint32_t count = content_encoding_entries_size_ + 1; + + ContentEncoding** const content_encoding_entries = + new (std::nothrow) ContentEncoding*[count]; // NOLINT + if (!content_encoding_entries) + return false; + + ContentEncoding* const content_encoding = + new (std::nothrow) ContentEncoding(); // NOLINT + if (!content_encoding) { + delete[] content_encoding_entries; + return false; + } + + for (uint32_t i = 0; i < content_encoding_entries_size_; ++i) { + content_encoding_entries[i] = content_encoding_entries_[i]; + } + + delete[] content_encoding_entries_; + + content_encoding_entries_ = content_encoding_entries; + content_encoding_entries_[content_encoding_entries_size_] = content_encoding; + content_encoding_entries_size_ = count; + return true; +} + +ContentEncoding* Track::GetContentEncodingByIndex(uint32_t index) const { + if (content_encoding_entries_ == NULL) + return NULL; + + if (index >= content_encoding_entries_size_) + return NULL; + + return content_encoding_entries_[index]; +} + +uint64_t Track::PayloadSize() const { + uint64_t size = + EbmlElementSize(libwebm::kMkvTrackNumber, static_cast(number_)); + size += EbmlElementSize(libwebm::kMkvTrackUID, static_cast(uid_)); + size += EbmlElementSize(libwebm::kMkvTrackType, static_cast(type_)); + if (codec_id_) + size += EbmlElementSize(libwebm::kMkvCodecID, codec_id_); + if (codec_private_) + size += EbmlElementSize(libwebm::kMkvCodecPrivate, codec_private_, + codec_private_length_); + if (language_) + size += EbmlElementSize(libwebm::kMkvLanguage, language_); + if (name_) + size += EbmlElementSize(libwebm::kMkvName, name_); + if (max_block_additional_id_) { + size += EbmlElementSize(libwebm::kMkvMaxBlockAdditionID, + static_cast(max_block_additional_id_)); + } + if (codec_delay_) { + size += EbmlElementSize(libwebm::kMkvCodecDelay, + static_cast(codec_delay_)); + } + if (seek_pre_roll_) { + size += EbmlElementSize(libwebm::kMkvSeekPreRoll, + static_cast(seek_pre_roll_)); + } + if (default_duration_) { + size += EbmlElementSize(libwebm::kMkvDefaultDuration, + static_cast(default_duration_)); + } + + if (content_encoding_entries_size_ > 0) { + uint64_t content_encodings_size = 0; + for (uint32_t i = 0; i < content_encoding_entries_size_; ++i) { + ContentEncoding* const encoding = content_encoding_entries_[i]; + content_encodings_size += encoding->Size(); + } + + size += EbmlMasterElementSize(libwebm::kMkvContentEncodings, + content_encodings_size) + + content_encodings_size; + } + + return size; +} + +uint64_t Track::Size() const { + uint64_t size = PayloadSize(); + size += EbmlMasterElementSize(libwebm::kMkvTrackEntry, size); + return size; +} + +bool Track::Write(IMkvWriter* writer) const { + if (!writer) + return false; + + // mandatory elements without a default value. + if (!type_ || !codec_id_) + return false; + + // |size| may be bigger than what is written out in this function because + // derived classes may write out more data in the Track element. + const uint64_t payload_size = PayloadSize(); + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvTrackEntry, payload_size)) + return false; + + uint64_t size = + EbmlElementSize(libwebm::kMkvTrackNumber, static_cast(number_)); + size += EbmlElementSize(libwebm::kMkvTrackUID, static_cast(uid_)); + size += EbmlElementSize(libwebm::kMkvTrackType, static_cast(type_)); + if (codec_id_) + size += EbmlElementSize(libwebm::kMkvCodecID, codec_id_); + if (codec_private_) + size += EbmlElementSize(libwebm::kMkvCodecPrivate, codec_private_, + static_cast(codec_private_length_)); + if (language_) + size += EbmlElementSize(libwebm::kMkvLanguage, language_); + if (name_) + size += EbmlElementSize(libwebm::kMkvName, name_); + if (max_block_additional_id_) + size += EbmlElementSize(libwebm::kMkvMaxBlockAdditionID, + static_cast(max_block_additional_id_)); + if (codec_delay_) + size += EbmlElementSize(libwebm::kMkvCodecDelay, + static_cast(codec_delay_)); + if (seek_pre_roll_) + size += EbmlElementSize(libwebm::kMkvSeekPreRoll, + static_cast(seek_pre_roll_)); + if (default_duration_) + size += EbmlElementSize(libwebm::kMkvDefaultDuration, + static_cast(default_duration_)); + + const int64_t payload_position = writer->Position(); + if (payload_position < 0) + return false; + + if (!WriteEbmlElement(writer, libwebm::kMkvTrackNumber, + static_cast(number_))) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvTrackUID, + static_cast(uid_))) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvTrackType, + static_cast(type_))) + return false; + if (max_block_additional_id_) { + if (!WriteEbmlElement(writer, libwebm::kMkvMaxBlockAdditionID, + static_cast(max_block_additional_id_))) { + return false; + } + } + if (codec_delay_) { + if (!WriteEbmlElement(writer, libwebm::kMkvCodecDelay, + static_cast(codec_delay_))) + return false; + } + if (seek_pre_roll_) { + if (!WriteEbmlElement(writer, libwebm::kMkvSeekPreRoll, + static_cast(seek_pre_roll_))) + return false; + } + if (default_duration_) { + if (!WriteEbmlElement(writer, libwebm::kMkvDefaultDuration, + static_cast(default_duration_))) + return false; + } + if (codec_id_) { + if (!WriteEbmlElement(writer, libwebm::kMkvCodecID, codec_id_)) + return false; + } + if (codec_private_) { + if (!WriteEbmlElement(writer, libwebm::kMkvCodecPrivate, codec_private_, + static_cast(codec_private_length_))) + return false; + } + if (language_) { + if (!WriteEbmlElement(writer, libwebm::kMkvLanguage, language_)) + return false; + } + if (name_) { + if (!WriteEbmlElement(writer, libwebm::kMkvName, name_)) + return false; + } + + int64_t stop_position = writer->Position(); + if (stop_position < 0 || + stop_position - payload_position != static_cast(size)) + return false; + + if (content_encoding_entries_size_ > 0) { + uint64_t content_encodings_size = 0; + for (uint32_t i = 0; i < content_encoding_entries_size_; ++i) { + ContentEncoding* const encoding = content_encoding_entries_[i]; + content_encodings_size += encoding->Size(); + } + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvContentEncodings, + content_encodings_size)) + return false; + + for (uint32_t i = 0; i < content_encoding_entries_size_; ++i) { + ContentEncoding* const encoding = content_encoding_entries_[i]; + if (!encoding->Write(writer)) + return false; + } + } + + stop_position = writer->Position(); + if (stop_position < 0) + return false; + return true; +} + +bool Track::SetCodecPrivate(const uint8_t* codec_private, uint64_t length) { + if (!codec_private || length < 1) + return false; + + delete[] codec_private_; + + codec_private_ = + new (std::nothrow) uint8_t[static_cast(length)]; // NOLINT + if (!codec_private_) + return false; + + memcpy(codec_private_, codec_private, static_cast(length)); + codec_private_length_ = length; + + return true; +} + +void Track::set_codec_id(const char* codec_id) { + if (codec_id) { + delete[] codec_id_; + + const size_t length = strlen(codec_id) + 1; + codec_id_ = new (std::nothrow) char[length]; // NOLINT + if (codec_id_) { +#ifdef _MSC_VER + strcpy_s(codec_id_, length, codec_id); +#else + strcpy(codec_id_, codec_id); +#endif + } + } +} + +// TODO(fgalligan): Vet the language parameter. +void Track::set_language(const char* language) { + if (language) { + delete[] language_; + + const size_t length = strlen(language) + 1; + language_ = new (std::nothrow) char[length]; // NOLINT + if (language_) { +#ifdef _MSC_VER + strcpy_s(language_, length, language); +#else + strcpy(language_, language); +#endif + } + } +} + +void Track::set_name(const char* name) { + if (name) { + delete[] name_; + + const size_t length = strlen(name) + 1; + name_ = new (std::nothrow) char[length]; // NOLINT + if (name_) { +#ifdef _MSC_VER + strcpy_s(name_, length, name); +#else + strcpy(name_, name); +#endif + } + } +} + +/////////////////////////////////////////////////////////////// +// +// Colour and its child elements + +uint64_t PrimaryChromaticity::PrimaryChromaticityPayloadSize( + libwebm::MkvId x_id, libwebm::MkvId y_id) const { + return EbmlElementSize(x_id, x) + EbmlElementSize(y_id, y); +} + +bool PrimaryChromaticity::Write(IMkvWriter* writer, libwebm::MkvId x_id, + libwebm::MkvId y_id) const { + return WriteEbmlElement(writer, x_id, x) && WriteEbmlElement(writer, y_id, y); +} + +uint64_t MasteringMetadata::MasteringMetadataSize() const { + uint64_t size = PayloadSize(); + + if (size > 0) + size += EbmlMasterElementSize(libwebm::kMkvMasteringMetadata, size); + + return size; +} + +bool MasteringMetadata::Write(IMkvWriter* writer) const { + const uint64_t size = PayloadSize(); + + // Don't write an empty element. + if (size == 0) + return true; + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvMasteringMetadata, size)) + return false; + if (luminance_max != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvLuminanceMax, luminance_max)) { + return false; + } + if (luminance_min != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvLuminanceMin, luminance_min)) { + return false; + } + if (r_ && + !r_->Write(writer, libwebm::kMkvPrimaryRChromaticityX, + libwebm::kMkvPrimaryRChromaticityY)) { + return false; + } + if (g_ && + !g_->Write(writer, libwebm::kMkvPrimaryGChromaticityX, + libwebm::kMkvPrimaryGChromaticityY)) { + return false; + } + if (b_ && + !b_->Write(writer, libwebm::kMkvPrimaryBChromaticityX, + libwebm::kMkvPrimaryBChromaticityY)) { + return false; + } + if (white_point_ && + !white_point_->Write(writer, libwebm::kMkvWhitePointChromaticityX, + libwebm::kMkvWhitePointChromaticityY)) { + return false; + } + + return true; +} + +bool MasteringMetadata::SetChromaticity( + const PrimaryChromaticity* r, const PrimaryChromaticity* g, + const PrimaryChromaticity* b, const PrimaryChromaticity* white_point) { + PrimaryChromaticityPtr r_ptr(NULL); + if (r) { + if (!CopyChromaticity(r, &r_ptr)) + return false; + } + PrimaryChromaticityPtr g_ptr(NULL); + if (g) { + if (!CopyChromaticity(g, &g_ptr)) + return false; + } + PrimaryChromaticityPtr b_ptr(NULL); + if (b) { + if (!CopyChromaticity(b, &b_ptr)) + return false; + } + PrimaryChromaticityPtr wp_ptr(NULL); + if (white_point) { + if (!CopyChromaticity(white_point, &wp_ptr)) + return false; + } + + r_ = r_ptr.release(); + g_ = g_ptr.release(); + b_ = b_ptr.release(); + white_point_ = wp_ptr.release(); + return true; +} + +uint64_t MasteringMetadata::PayloadSize() const { + uint64_t size = 0; + + if (luminance_max != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvLuminanceMax, luminance_max); + if (luminance_min != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvLuminanceMin, luminance_min); + + if (r_) { + size += r_->PrimaryChromaticityPayloadSize( + libwebm::kMkvPrimaryRChromaticityX, libwebm::kMkvPrimaryRChromaticityY); + } + if (g_) { + size += g_->PrimaryChromaticityPayloadSize( + libwebm::kMkvPrimaryGChromaticityX, libwebm::kMkvPrimaryGChromaticityY); + } + if (b_) { + size += b_->PrimaryChromaticityPayloadSize( + libwebm::kMkvPrimaryBChromaticityX, libwebm::kMkvPrimaryBChromaticityY); + } + if (white_point_) { + size += white_point_->PrimaryChromaticityPayloadSize( + libwebm::kMkvWhitePointChromaticityX, + libwebm::kMkvWhitePointChromaticityY); + } + + return size; +} + +uint64_t Colour::ColourSize() const { + uint64_t size = PayloadSize(); + + if (size > 0) + size += EbmlMasterElementSize(libwebm::kMkvColour, size); + + return size; +} + +bool Colour::Write(IMkvWriter* writer) const { + const uint64_t size = PayloadSize(); + + // Don't write an empty element. + if (size == 0) + return true; + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvColour, size)) + return false; + + if (matrix_coefficients != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvMatrixCoefficients, + static_cast(matrix_coefficients))) { + return false; + } + if (bits_per_channel != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvBitsPerChannel, + static_cast(bits_per_channel))) { + return false; + } + if (chroma_subsampling_horz != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvChromaSubsamplingHorz, + static_cast(chroma_subsampling_horz))) { + return false; + } + if (chroma_subsampling_vert != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvChromaSubsamplingVert, + static_cast(chroma_subsampling_vert))) { + return false; + } + + if (cb_subsampling_horz != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvCbSubsamplingHorz, + static_cast(cb_subsampling_horz))) { + return false; + } + if (cb_subsampling_vert != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvCbSubsamplingVert, + static_cast(cb_subsampling_vert))) { + return false; + } + if (chroma_siting_horz != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvChromaSitingHorz, + static_cast(chroma_siting_horz))) { + return false; + } + if (chroma_siting_vert != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvChromaSitingVert, + static_cast(chroma_siting_vert))) { + return false; + } + if (range != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvRange, + static_cast(range))) { + return false; + } + if (transfer_characteristics != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvTransferCharacteristics, + static_cast(transfer_characteristics))) { + return false; + } + if (primaries != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvPrimaries, + static_cast(primaries))) { + return false; + } + if (max_cll != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvMaxCLL, + static_cast(max_cll))) { + return false; + } + if (max_fall != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvMaxFALL, + static_cast(max_fall))) { + return false; + } + + if (mastering_metadata_ && !mastering_metadata_->Write(writer)) + return false; + + return true; +} + +bool Colour::SetMasteringMetadata(const MasteringMetadata& mastering_metadata) { + std::unique_ptr mm_ptr(new MasteringMetadata()); + if (!mm_ptr.get()) + return false; + + mm_ptr->luminance_max = mastering_metadata.luminance_max; + mm_ptr->luminance_min = mastering_metadata.luminance_min; + + if (!mm_ptr->SetChromaticity(mastering_metadata.r(), mastering_metadata.g(), + mastering_metadata.b(), + mastering_metadata.white_point())) { + return false; + } + + delete mastering_metadata_; + mastering_metadata_ = mm_ptr.release(); + return true; +} + +uint64_t Colour::PayloadSize() const { + uint64_t size = 0; + + if (matrix_coefficients != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvMatrixCoefficients, + static_cast(matrix_coefficients)); + if (bits_per_channel != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvBitsPerChannel, + static_cast(bits_per_channel)); + if (chroma_subsampling_horz != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvChromaSubsamplingHorz, + static_cast(chroma_subsampling_horz)); + if (chroma_subsampling_vert != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvChromaSubsamplingVert, + static_cast(chroma_subsampling_vert)); + if (cb_subsampling_horz != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvCbSubsamplingHorz, + static_cast(cb_subsampling_horz)); + if (cb_subsampling_vert != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvCbSubsamplingVert, + static_cast(cb_subsampling_vert)); + if (chroma_siting_horz != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvChromaSitingHorz, + static_cast(chroma_siting_horz)); + if (chroma_siting_vert != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvChromaSitingVert, + static_cast(chroma_siting_vert)); + if (range != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvRange, static_cast(range)); + if (transfer_characteristics != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvTransferCharacteristics, + static_cast(transfer_characteristics)); + if (primaries != kValueNotPresent) + size += + EbmlElementSize(libwebm::kMkvPrimaries, static_cast(primaries)); + if (max_cll != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvMaxCLL, static_cast(max_cll)); + if (max_fall != kValueNotPresent) + size += + EbmlElementSize(libwebm::kMkvMaxFALL, static_cast(max_fall)); + + if (mastering_metadata_) + size += mastering_metadata_->MasteringMetadataSize(); + + return size; +} + +/////////////////////////////////////////////////////////////// +// +// VideoTrack Class + +VideoTrack::VideoTrack(unsigned int* seed) + : Track(seed), + display_height_(0), + display_width_(0), + crop_left_(0), + crop_right_(0), + crop_top_(0), + crop_bottom_(0), + frame_rate_(0.0), + height_(0), + stereo_mode_(0), + alpha_mode_(0), + width_(0), + colour_(NULL) {} + +VideoTrack::~VideoTrack() { delete colour_; } + +bool VideoTrack::SetStereoMode(uint64_t stereo_mode) { + if (stereo_mode != kMono && stereo_mode != kSideBySideLeftIsFirst && + stereo_mode != kTopBottomRightIsFirst && + stereo_mode != kTopBottomLeftIsFirst && + stereo_mode != kSideBySideRightIsFirst) + return false; + + stereo_mode_ = stereo_mode; + return true; +} + +bool VideoTrack::SetAlphaMode(uint64_t alpha_mode) { + if (alpha_mode != kNoAlpha && alpha_mode != kAlpha) + return false; + + alpha_mode_ = alpha_mode; + return true; +} + +uint64_t VideoTrack::PayloadSize() const { + const uint64_t parent_size = Track::PayloadSize(); + + uint64_t size = VideoPayloadSize(); + size += EbmlMasterElementSize(libwebm::kMkvVideo, size); + + return parent_size + size; +} + +bool VideoTrack::Write(IMkvWriter* writer) const { + if (!Track::Write(writer)) + return false; + + const uint64_t size = VideoPayloadSize(); + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvVideo, size)) + return false; + + const int64_t payload_position = writer->Position(); + if (payload_position < 0) + return false; + + if (!WriteEbmlElement(writer, libwebm::kMkvPixelWidth, + static_cast(width_))) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvPixelHeight, + static_cast(height_))) + return false; + if (display_width_ > 0) { + if (!WriteEbmlElement(writer, libwebm::kMkvDisplayWidth, + static_cast(display_width_))) + return false; + } + if (display_height_ > 0) { + if (!WriteEbmlElement(writer, libwebm::kMkvDisplayHeight, + static_cast(display_height_))) + return false; + } + if (crop_left_ > 0) { + if (!WriteEbmlElement(writer, libwebm::kMkvPixelCropLeft, + static_cast(crop_left_))) + return false; + } + if (crop_right_ > 0) { + if (!WriteEbmlElement(writer, libwebm::kMkvPixelCropRight, + static_cast(crop_right_))) + return false; + } + if (crop_top_ > 0) { + if (!WriteEbmlElement(writer, libwebm::kMkvPixelCropTop, + static_cast(crop_top_))) + return false; + } + if (crop_bottom_ > 0) { + if (!WriteEbmlElement(writer, libwebm::kMkvPixelCropBottom, + static_cast(crop_bottom_))) + return false; + } + if (stereo_mode_ > kMono) { + if (!WriteEbmlElement(writer, libwebm::kMkvStereoMode, + static_cast(stereo_mode_))) + return false; + } + if (alpha_mode_ > kNoAlpha) { + if (!WriteEbmlElement(writer, libwebm::kMkvAlphaMode, + static_cast(alpha_mode_))) + return false; + } + if (frame_rate_ > 0.0) { + if (!WriteEbmlElement(writer, libwebm::kMkvFrameRate, + static_cast(frame_rate_))) { + return false; + } + } + if (colour_) { + if (!colour_->Write(writer)) + return false; + } + + const int64_t stop_position = writer->Position(); + if (stop_position < 0 || + stop_position - payload_position != static_cast(size)) { + return false; + } + + return true; +} + +bool VideoTrack::SetColour(const Colour& colour) { + std::unique_ptr colour_ptr(new Colour()); + if (!colour_ptr.get()) + return false; + + if (colour.mastering_metadata()) { + if (!colour_ptr->SetMasteringMetadata(*colour.mastering_metadata())) + return false; + } + + colour_ptr->matrix_coefficients = colour.matrix_coefficients; + colour_ptr->bits_per_channel = colour.bits_per_channel; + colour_ptr->chroma_subsampling_horz = colour.chroma_subsampling_horz; + colour_ptr->chroma_subsampling_vert = colour.chroma_subsampling_vert; + colour_ptr->cb_subsampling_horz = colour.cb_subsampling_horz; + colour_ptr->cb_subsampling_vert = colour.cb_subsampling_vert; + colour_ptr->chroma_siting_horz = colour.chroma_siting_horz; + colour_ptr->chroma_siting_vert = colour.chroma_siting_vert; + colour_ptr->range = colour.range; + colour_ptr->transfer_characteristics = colour.transfer_characteristics; + colour_ptr->primaries = colour.primaries; + colour_ptr->max_cll = colour.max_cll; + colour_ptr->max_fall = colour.max_fall; + colour_ = colour_ptr.release(); + return true; +} + +uint64_t VideoTrack::VideoPayloadSize() const { + uint64_t size = + EbmlElementSize(libwebm::kMkvPixelWidth, static_cast(width_)); + size += + EbmlElementSize(libwebm::kMkvPixelHeight, static_cast(height_)); + if (display_width_ > 0) + size += EbmlElementSize(libwebm::kMkvDisplayWidth, + static_cast(display_width_)); + if (display_height_ > 0) + size += EbmlElementSize(libwebm::kMkvDisplayHeight, + static_cast(display_height_)); + if (crop_left_ > 0) + size += EbmlElementSize(libwebm::kMkvPixelCropLeft, + static_cast(crop_left_)); + if (crop_right_ > 0) + size += EbmlElementSize(libwebm::kMkvPixelCropRight, + static_cast(crop_right_)); + if (crop_top_ > 0) + size += EbmlElementSize(libwebm::kMkvPixelCropTop, + static_cast(crop_top_)); + if (crop_bottom_ > 0) + size += EbmlElementSize(libwebm::kMkvPixelCropBottom, + static_cast(crop_bottom_)); + if (stereo_mode_ > kMono) + size += EbmlElementSize(libwebm::kMkvStereoMode, + static_cast(stereo_mode_)); + if (alpha_mode_ > kNoAlpha) + size += EbmlElementSize(libwebm::kMkvAlphaMode, + static_cast(alpha_mode_)); + if (frame_rate_ > 0.0) + size += EbmlElementSize(libwebm::kMkvFrameRate, + static_cast(frame_rate_)); + if (colour_) + size += colour_->ColourSize(); + + return size; +} + +/////////////////////////////////////////////////////////////// +// +// AudioTrack Class + +AudioTrack::AudioTrack(unsigned int* seed) + : Track(seed), bit_depth_(0), channels_(1), sample_rate_(0.0) {} + +AudioTrack::~AudioTrack() {} + +uint64_t AudioTrack::PayloadSize() const { + const uint64_t parent_size = Track::PayloadSize(); + + uint64_t size = EbmlElementSize(libwebm::kMkvSamplingFrequency, + static_cast(sample_rate_)); + size += + EbmlElementSize(libwebm::kMkvChannels, static_cast(channels_)); + if (bit_depth_ > 0) + size += + EbmlElementSize(libwebm::kMkvBitDepth, static_cast(bit_depth_)); + size += EbmlMasterElementSize(libwebm::kMkvAudio, size); + + return parent_size + size; +} + +bool AudioTrack::Write(IMkvWriter* writer) const { + if (!Track::Write(writer)) + return false; + + // Calculate AudioSettings size. + uint64_t size = EbmlElementSize(libwebm::kMkvSamplingFrequency, + static_cast(sample_rate_)); + size += + EbmlElementSize(libwebm::kMkvChannels, static_cast(channels_)); + if (bit_depth_ > 0) + size += + EbmlElementSize(libwebm::kMkvBitDepth, static_cast(bit_depth_)); + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvAudio, size)) + return false; + + const int64_t payload_position = writer->Position(); + if (payload_position < 0) + return false; + + if (!WriteEbmlElement(writer, libwebm::kMkvSamplingFrequency, + static_cast(sample_rate_))) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvChannels, + static_cast(channels_))) + return false; + if (bit_depth_ > 0) + if (!WriteEbmlElement(writer, libwebm::kMkvBitDepth, + static_cast(bit_depth_))) + return false; + + const int64_t stop_position = writer->Position(); + if (stop_position < 0 || + stop_position - payload_position != static_cast(size)) + return false; + + return true; +} + +/////////////////////////////////////////////////////////////// +// +// Tracks Class + +const char Tracks::kOpusCodecId[] = "A_OPUS"; +const char Tracks::kVorbisCodecId[] = "A_VORBIS"; +const char Tracks::kVp8CodecId[] = "V_VP8"; +const char Tracks::kVp9CodecId[] = "V_VP9"; +const char Tracks::kVp10CodecId[] = "V_VP10"; +const char Tracks::kWebVttCaptionsId[] = "D_WEBVTT/CAPTIONS"; +const char Tracks::kWebVttDescriptionsId[] = "D_WEBVTT/DESCRIPTIONS"; +const char Tracks::kWebVttMetadataId[] = "D_WEBVTT/METADATA"; +const char Tracks::kWebVttSubtitlesId[] = "D_WEBVTT/SUBTITLES"; + +Tracks::Tracks() + : track_entries_(NULL), track_entries_size_(0), wrote_tracks_(false) {} + +Tracks::~Tracks() { + if (track_entries_) { + for (uint32_t i = 0; i < track_entries_size_; ++i) { + Track* const track = track_entries_[i]; + delete track; + } + delete[] track_entries_; + } +} + +bool Tracks::AddTrack(Track* track, int32_t number) { + if (number < 0 || wrote_tracks_) + return false; + + // This muxer only supports track numbers in the range [1, 126], in + // order to be able (to use Matroska integer representation) to + // serialize the block header (of which the track number is a part) + // for a frame using exactly 4 bytes. + + if (number > 0x7E) + return false; + + uint32_t track_num = number; + + if (track_num > 0) { + // Check to make sure a track does not already have |track_num|. + for (uint32_t i = 0; i < track_entries_size_; ++i) { + if (track_entries_[i]->number() == track_num) + return false; + } + } + + const uint32_t count = track_entries_size_ + 1; + + Track** const track_entries = new (std::nothrow) Track*[count]; // NOLINT + if (!track_entries) + return false; + + for (uint32_t i = 0; i < track_entries_size_; ++i) { + track_entries[i] = track_entries_[i]; + } + + delete[] track_entries_; + + // Find the lowest availible track number > 0. + if (track_num == 0) { + track_num = count; + + // Check to make sure a track does not already have |track_num|. + bool exit = false; + do { + exit = true; + for (uint32_t i = 0; i < track_entries_size_; ++i) { + if (track_entries[i]->number() == track_num) { + track_num++; + exit = false; + break; + } + } + } while (!exit); + } + track->set_number(track_num); + + track_entries_ = track_entries; + track_entries_[track_entries_size_] = track; + track_entries_size_ = count; + return true; +} + +const Track* Tracks::GetTrackByIndex(uint32_t index) const { + if (track_entries_ == NULL) + return NULL; + + if (index >= track_entries_size_) + return NULL; + + return track_entries_[index]; +} + +Track* Tracks::GetTrackByNumber(uint64_t track_number) const { + const int32_t count = track_entries_size(); + for (int32_t i = 0; i < count; ++i) { + if (track_entries_[i]->number() == track_number) + return track_entries_[i]; + } + + return NULL; +} + +bool Tracks::TrackIsAudio(uint64_t track_number) const { + const Track* const track = GetTrackByNumber(track_number); + + if (track->type() == kAudio) + return true; + + return false; +} + +bool Tracks::TrackIsVideo(uint64_t track_number) const { + const Track* const track = GetTrackByNumber(track_number); + + if (track->type() == kVideo) + return true; + + return false; +} + +bool Tracks::Write(IMkvWriter* writer) const { + uint64_t size = 0; + const int32_t count = track_entries_size(); + for (int32_t i = 0; i < count; ++i) { + const Track* const track = GetTrackByIndex(i); + + if (!track) + return false; + + size += track->Size(); + } + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvTracks, size)) + return false; + + const int64_t payload_position = writer->Position(); + if (payload_position < 0) + return false; + + for (int32_t i = 0; i < count; ++i) { + const Track* const track = GetTrackByIndex(i); + if (!track->Write(writer)) + return false; + } + + const int64_t stop_position = writer->Position(); + if (stop_position < 0 || + stop_position - payload_position != static_cast(size)) + return false; + + wrote_tracks_ = true; + return true; +} + +/////////////////////////////////////////////////////////////// +// +// Chapter Class + +bool Chapter::set_id(const char* id) { return StrCpy(id, &id_); } + +void Chapter::set_time(const Segment& segment, uint64_t start_ns, + uint64_t end_ns) { + const SegmentInfo* const info = segment.GetSegmentInfo(); + const uint64_t timecode_scale = info->timecode_scale(); + start_timecode_ = start_ns / timecode_scale; + end_timecode_ = end_ns / timecode_scale; +} + +bool Chapter::add_string(const char* title, const char* language, + const char* country) { + if (!ExpandDisplaysArray()) + return false; + + Display& d = displays_[displays_count_++]; + d.Init(); + + if (!d.set_title(title)) + return false; + + if (!d.set_language(language)) + return false; + + if (!d.set_country(country)) + return false; + + return true; +} + +Chapter::Chapter() { + // This ctor only constructs the object. Proper initialization is + // done in Init() (called in Chapters::AddChapter()). The only + // reason we bother implementing this ctor is because we had to + // declare it as private (along with the dtor), in order to prevent + // clients from creating Chapter instances (a privelege we grant + // only to the Chapters class). Doing no initialization here also + // means that creating arrays of chapter objects is more efficient, + // because we only initialize each new chapter object as it becomes + // active on the array. +} + +Chapter::~Chapter() {} + +void Chapter::Init(unsigned int* seed) { + id_ = NULL; + start_timecode_ = 0; + end_timecode_ = 0; + displays_ = NULL; + displays_size_ = 0; + displays_count_ = 0; + uid_ = MakeUID(seed); +} + +void Chapter::ShallowCopy(Chapter* dst) const { + dst->id_ = id_; + dst->start_timecode_ = start_timecode_; + dst->end_timecode_ = end_timecode_; + dst->uid_ = uid_; + dst->displays_ = displays_; + dst->displays_size_ = displays_size_; + dst->displays_count_ = displays_count_; +} + +void Chapter::Clear() { + StrCpy(NULL, &id_); + + while (displays_count_ > 0) { + Display& d = displays_[--displays_count_]; + d.Clear(); + } + + delete[] displays_; + displays_ = NULL; + + displays_size_ = 0; +} + +bool Chapter::ExpandDisplaysArray() { + if (displays_size_ > displays_count_) + return true; // nothing to do yet + + const int size = (displays_size_ == 0) ? 1 : 2 * displays_size_; + + Display* const displays = new (std::nothrow) Display[size]; // NOLINT + if (displays == NULL) + return false; + + for (int idx = 0; idx < displays_count_; ++idx) { + displays[idx] = displays_[idx]; // shallow copy + } + + delete[] displays_; + + displays_ = displays; + displays_size_ = size; + + return true; +} + +uint64_t Chapter::WriteAtom(IMkvWriter* writer) const { + uint64_t payload_size = + EbmlElementSize(libwebm::kMkvChapterStringUID, id_) + + EbmlElementSize(libwebm::kMkvChapterUID, static_cast(uid_)) + + EbmlElementSize(libwebm::kMkvChapterTimeStart, + static_cast(start_timecode_)) + + EbmlElementSize(libwebm::kMkvChapterTimeEnd, + static_cast(end_timecode_)); + + for (int idx = 0; idx < displays_count_; ++idx) { + const Display& d = displays_[idx]; + payload_size += d.WriteDisplay(NULL); + } + + const uint64_t atom_size = + EbmlMasterElementSize(libwebm::kMkvChapterAtom, payload_size) + + payload_size; + + if (writer == NULL) + return atom_size; + + const int64_t start = writer->Position(); + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvChapterAtom, payload_size)) + return 0; + + if (!WriteEbmlElement(writer, libwebm::kMkvChapterStringUID, id_)) + return 0; + + if (!WriteEbmlElement(writer, libwebm::kMkvChapterUID, + static_cast(uid_))) + return 0; + + if (!WriteEbmlElement(writer, libwebm::kMkvChapterTimeStart, + static_cast(start_timecode_))) + return 0; + + if (!WriteEbmlElement(writer, libwebm::kMkvChapterTimeEnd, + static_cast(end_timecode_))) + return 0; + + for (int idx = 0; idx < displays_count_; ++idx) { + const Display& d = displays_[idx]; + + if (!d.WriteDisplay(writer)) + return 0; + } + + const int64_t stop = writer->Position(); + + if (stop >= start && uint64_t(stop - start) != atom_size) + return 0; + + return atom_size; +} + +void Chapter::Display::Init() { + title_ = NULL; + language_ = NULL; + country_ = NULL; +} + +void Chapter::Display::Clear() { + StrCpy(NULL, &title_); + StrCpy(NULL, &language_); + StrCpy(NULL, &country_); +} + +bool Chapter::Display::set_title(const char* title) { + return StrCpy(title, &title_); +} + +bool Chapter::Display::set_language(const char* language) { + return StrCpy(language, &language_); +} + +bool Chapter::Display::set_country(const char* country) { + return StrCpy(country, &country_); +} + +uint64_t Chapter::Display::WriteDisplay(IMkvWriter* writer) const { + uint64_t payload_size = EbmlElementSize(libwebm::kMkvChapString, title_); + + if (language_) + payload_size += EbmlElementSize(libwebm::kMkvChapLanguage, language_); + + if (country_) + payload_size += EbmlElementSize(libwebm::kMkvChapCountry, country_); + + const uint64_t display_size = + EbmlMasterElementSize(libwebm::kMkvChapterDisplay, payload_size) + + payload_size; + + if (writer == NULL) + return display_size; + + const int64_t start = writer->Position(); + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvChapterDisplay, + payload_size)) + return 0; + + if (!WriteEbmlElement(writer, libwebm::kMkvChapString, title_)) + return 0; + + if (language_) { + if (!WriteEbmlElement(writer, libwebm::kMkvChapLanguage, language_)) + return 0; + } + + if (country_) { + if (!WriteEbmlElement(writer, libwebm::kMkvChapCountry, country_)) + return 0; + } + + const int64_t stop = writer->Position(); + + if (stop >= start && uint64_t(stop - start) != display_size) + return 0; + + return display_size; +} + +/////////////////////////////////////////////////////////////// +// +// Chapters Class + +Chapters::Chapters() : chapters_size_(0), chapters_count_(0), chapters_(NULL) {} + +Chapters::~Chapters() { + while (chapters_count_ > 0) { + Chapter& chapter = chapters_[--chapters_count_]; + chapter.Clear(); + } + + delete[] chapters_; + chapters_ = NULL; +} + +int Chapters::Count() const { return chapters_count_; } + +Chapter* Chapters::AddChapter(unsigned int* seed) { + if (!ExpandChaptersArray()) + return NULL; + + Chapter& chapter = chapters_[chapters_count_++]; + chapter.Init(seed); + + return &chapter; +} + +bool Chapters::Write(IMkvWriter* writer) const { + if (writer == NULL) + return false; + + const uint64_t payload_size = WriteEdition(NULL); // return size only + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvChapters, payload_size)) + return false; + + const int64_t start = writer->Position(); + + if (WriteEdition(writer) == 0) // error + return false; + + const int64_t stop = writer->Position(); + + if (stop >= start && uint64_t(stop - start) != payload_size) + return false; + + return true; +} + +bool Chapters::ExpandChaptersArray() { + if (chapters_size_ > chapters_count_) + return true; // nothing to do yet + + const int size = (chapters_size_ == 0) ? 1 : 2 * chapters_size_; + + Chapter* const chapters = new (std::nothrow) Chapter[size]; // NOLINT + if (chapters == NULL) + return false; + + for (int idx = 0; idx < chapters_count_; ++idx) { + const Chapter& src = chapters_[idx]; + Chapter* const dst = chapters + idx; + src.ShallowCopy(dst); + } + + delete[] chapters_; + + chapters_ = chapters; + chapters_size_ = size; + + return true; +} + +uint64_t Chapters::WriteEdition(IMkvWriter* writer) const { + uint64_t payload_size = 0; + + for (int idx = 0; idx < chapters_count_; ++idx) { + const Chapter& chapter = chapters_[idx]; + payload_size += chapter.WriteAtom(NULL); + } + + const uint64_t edition_size = + EbmlMasterElementSize(libwebm::kMkvEditionEntry, payload_size) + + payload_size; + + if (writer == NULL) // return size only + return edition_size; + + const int64_t start = writer->Position(); + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvEditionEntry, payload_size)) + return 0; // error + + for (int idx = 0; idx < chapters_count_; ++idx) { + const Chapter& chapter = chapters_[idx]; + + const uint64_t chapter_size = chapter.WriteAtom(writer); + if (chapter_size == 0) // error + return 0; + } + + const int64_t stop = writer->Position(); + + if (stop >= start && uint64_t(stop - start) != edition_size) + return 0; + + return edition_size; +} + +// Tag Class + +bool Tag::add_simple_tag(const char* tag_name, const char* tag_string) { + if (!ExpandSimpleTagsArray()) + return false; + + SimpleTag& st = simple_tags_[simple_tags_count_++]; + st.Init(); + + if (!st.set_tag_name(tag_name)) + return false; + + if (!st.set_tag_string(tag_string)) + return false; + + return true; +} + +Tag::Tag() { + simple_tags_ = NULL; + simple_tags_size_ = 0; + simple_tags_count_ = 0; +} + +Tag::~Tag() {} + +void Tag::ShallowCopy(Tag* dst) const { + dst->simple_tags_ = simple_tags_; + dst->simple_tags_size_ = simple_tags_size_; + dst->simple_tags_count_ = simple_tags_count_; +} + +void Tag::Clear() { + while (simple_tags_count_ > 0) { + SimpleTag& st = simple_tags_[--simple_tags_count_]; + st.Clear(); + } + + delete[] simple_tags_; + simple_tags_ = NULL; + + simple_tags_size_ = 0; +} + +bool Tag::ExpandSimpleTagsArray() { + if (simple_tags_size_ > simple_tags_count_) + return true; // nothing to do yet + + const int size = (simple_tags_size_ == 0) ? 1 : 2 * simple_tags_size_; + + SimpleTag* const simple_tags = new (std::nothrow) SimpleTag[size]; // NOLINT + if (simple_tags == NULL) + return false; + + for (int idx = 0; idx < simple_tags_count_; ++idx) { + simple_tags[idx] = simple_tags_[idx]; // shallow copy + } + + delete[] simple_tags_; + + simple_tags_ = simple_tags; + simple_tags_size_ = size; + + return true; +} + +uint64_t Tag::Write(IMkvWriter* writer) const { + uint64_t payload_size = 0; + + for (int idx = 0; idx < simple_tags_count_; ++idx) { + const SimpleTag& st = simple_tags_[idx]; + payload_size += st.Write(NULL); + } + + const uint64_t tag_size = + EbmlMasterElementSize(libwebm::kMkvTag, payload_size) + payload_size; + + if (writer == NULL) + return tag_size; + + const int64_t start = writer->Position(); + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvTag, payload_size)) + return 0; + + for (int idx = 0; idx < simple_tags_count_; ++idx) { + const SimpleTag& st = simple_tags_[idx]; + + if (!st.Write(writer)) + return 0; + } + + const int64_t stop = writer->Position(); + + if (stop >= start && uint64_t(stop - start) != tag_size) + return 0; + + return tag_size; +} + +// Tag::SimpleTag + +void Tag::SimpleTag::Init() { + tag_name_ = NULL; + tag_string_ = NULL; +} + +void Tag::SimpleTag::Clear() { + StrCpy(NULL, &tag_name_); + StrCpy(NULL, &tag_string_); +} + +bool Tag::SimpleTag::set_tag_name(const char* tag_name) { + return StrCpy(tag_name, &tag_name_); +} + +bool Tag::SimpleTag::set_tag_string(const char* tag_string) { + return StrCpy(tag_string, &tag_string_); +} + +uint64_t Tag::SimpleTag::Write(IMkvWriter* writer) const { + uint64_t payload_size = EbmlElementSize(libwebm::kMkvTagName, tag_name_); + + payload_size += EbmlElementSize(libwebm::kMkvTagString, tag_string_); + + const uint64_t simple_tag_size = + EbmlMasterElementSize(libwebm::kMkvSimpleTag, payload_size) + + payload_size; + + if (writer == NULL) + return simple_tag_size; + + const int64_t start = writer->Position(); + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvSimpleTag, payload_size)) + return 0; + + if (!WriteEbmlElement(writer, libwebm::kMkvTagName, tag_name_)) + return 0; + + if (!WriteEbmlElement(writer, libwebm::kMkvTagString, tag_string_)) + return 0; + + const int64_t stop = writer->Position(); + + if (stop >= start && uint64_t(stop - start) != simple_tag_size) + return 0; + + return simple_tag_size; +} + +// Tags Class + +Tags::Tags() : tags_size_(0), tags_count_(0), tags_(NULL) {} + +Tags::~Tags() { + while (tags_count_ > 0) { + Tag& tag = tags_[--tags_count_]; + tag.Clear(); + } + + delete[] tags_; + tags_ = NULL; +} + +int Tags::Count() const { return tags_count_; } + +Tag* Tags::AddTag() { + if (!ExpandTagsArray()) + return NULL; + + Tag& tag = tags_[tags_count_++]; + + return &tag; +} + +bool Tags::Write(IMkvWriter* writer) const { + if (writer == NULL) + return false; + + uint64_t payload_size = 0; + + for (int idx = 0; idx < tags_count_; ++idx) { + const Tag& tag = tags_[idx]; + payload_size += tag.Write(NULL); + } + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvTags, payload_size)) + return false; + + const int64_t start = writer->Position(); + + for (int idx = 0; idx < tags_count_; ++idx) { + const Tag& tag = tags_[idx]; + + const uint64_t tag_size = tag.Write(writer); + if (tag_size == 0) // error + return 0; + } + + const int64_t stop = writer->Position(); + + if (stop >= start && uint64_t(stop - start) != payload_size) + return false; + + return true; +} + +bool Tags::ExpandTagsArray() { + if (tags_size_ > tags_count_) + return true; // nothing to do yet + + const int size = (tags_size_ == 0) ? 1 : 2 * tags_size_; + + Tag* const tags = new (std::nothrow) Tag[size]; // NOLINT + if (tags == NULL) + return false; + + for (int idx = 0; idx < tags_count_; ++idx) { + const Tag& src = tags_[idx]; + Tag* const dst = tags + idx; + src.ShallowCopy(dst); + } + + delete[] tags_; + + tags_ = tags; + tags_size_ = size; + + return true; +} + +/////////////////////////////////////////////////////////////// +// +// Cluster class + +Cluster::Cluster(uint64_t timecode, int64_t cues_pos, uint64_t timecode_scale, + bool write_last_frame_with_duration, bool fixed_size_timecode) + : blocks_added_(0), + finalized_(false), + fixed_size_timecode_(fixed_size_timecode), + header_written_(false), + payload_size_(0), + position_for_cues_(cues_pos), + size_position_(-1), + timecode_(timecode), + timecode_scale_(timecode_scale), + write_last_frame_with_duration_(write_last_frame_with_duration), + writer_(NULL) {} + +Cluster::~Cluster() { + // Delete any stored frames that are left behind. This will happen if the + // Cluster was not Finalized for whatever reason. + while (!stored_frames_.empty()) { + while (!stored_frames_.begin()->second.empty()) { + delete stored_frames_.begin()->second.front(); + stored_frames_.begin()->second.pop_front(); + } + stored_frames_.erase(stored_frames_.begin()->first); + } +} + +bool Cluster::Init(IMkvWriter* ptr_writer) { + if (!ptr_writer) { + return false; + } + writer_ = ptr_writer; + return true; +} + +bool Cluster::AddFrame(const Frame* const frame) { + return QueueOrWriteFrame(frame); +} + +bool Cluster::AddFrame(const uint8_t* data, uint64_t length, + uint64_t track_number, uint64_t abs_timecode, + bool is_key) { + Frame frame; + if (!frame.Init(data, length)) + return false; + frame.set_track_number(track_number); + frame.set_timestamp(abs_timecode); + frame.set_is_key(is_key); + return QueueOrWriteFrame(&frame); +} + +bool Cluster::AddFrameWithAdditional(const uint8_t* data, uint64_t length, + const uint8_t* additional, + uint64_t additional_length, + uint64_t add_id, uint64_t track_number, + uint64_t abs_timecode, bool is_key) { + if (!additional || additional_length == 0) { + return false; + } + Frame frame; + if (!frame.Init(data, length) || + !frame.AddAdditionalData(additional, additional_length, add_id)) { + return false; + } + frame.set_track_number(track_number); + frame.set_timestamp(abs_timecode); + frame.set_is_key(is_key); + return QueueOrWriteFrame(&frame); +} + +bool Cluster::AddFrameWithDiscardPadding(const uint8_t* data, uint64_t length, + int64_t discard_padding, + uint64_t track_number, + uint64_t abs_timecode, bool is_key) { + Frame frame; + if (!frame.Init(data, length)) + return false; + frame.set_discard_padding(discard_padding); + frame.set_track_number(track_number); + frame.set_timestamp(abs_timecode); + frame.set_is_key(is_key); + return QueueOrWriteFrame(&frame); +} + +bool Cluster::AddMetadata(const uint8_t* data, uint64_t length, + uint64_t track_number, uint64_t abs_timecode, + uint64_t duration_timecode) { + Frame frame; + if (!frame.Init(data, length)) + return false; + frame.set_track_number(track_number); + frame.set_timestamp(abs_timecode); + frame.set_duration(duration_timecode); + frame.set_is_key(true); // All metadata blocks are keyframes. + return QueueOrWriteFrame(&frame); +} + +void Cluster::AddPayloadSize(uint64_t size) { payload_size_ += size; } + +bool Cluster::Finalize() { + return !write_last_frame_with_duration_ && Finalize(false, 0); +} + +bool Cluster::Finalize(bool set_last_frame_duration, uint64_t duration) { + if (!writer_ || finalized_) + return false; + + if (write_last_frame_with_duration_) { + // Write out held back Frames. This essentially performs a k-way merge + // across all tracks in the increasing order of timestamps. + while (!stored_frames_.empty()) { + Frame* frame = stored_frames_.begin()->second.front(); + + // Get the next frame to write (frame with least timestamp across all + // tracks). + for (FrameMapIterator frames_iterator = ++stored_frames_.begin(); + frames_iterator != stored_frames_.end(); ++frames_iterator) { + if (frames_iterator->second.front()->timestamp() < frame->timestamp()) { + frame = frames_iterator->second.front(); + } + } + + // Set the duration if it's the last frame for the track. + if (set_last_frame_duration && + stored_frames_[frame->track_number()].size() == 1 && + !frame->duration_set()) { + frame->set_duration(duration - frame->timestamp()); + if (!frame->is_key() && !frame->reference_block_timestamp_set()) { + frame->set_reference_block_timestamp( + last_block_timestamp_[frame->track_number()]); + } + } + + // Write the frame and remove it from |stored_frames_|. + const bool wrote_frame = DoWriteFrame(frame); + stored_frames_[frame->track_number()].pop_front(); + if (stored_frames_[frame->track_number()].empty()) { + stored_frames_.erase(frame->track_number()); + } + delete frame; + if (!wrote_frame) + return false; + } + } + + if (size_position_ == -1) + return false; + + if (writer_->Seekable()) { + const int64_t pos = writer_->Position(); + + if (writer_->Position(size_position_)) + return false; + + if (WriteUIntSize(writer_, payload_size(), 8)) + return false; + + if (writer_->Position(pos)) + return false; + } + + finalized_ = true; + + return true; +} + +uint64_t Cluster::Size() const { + const uint64_t element_size = + EbmlMasterElementSize(libwebm::kMkvCluster, 0xFFFFFFFFFFFFFFFFULL) + + payload_size_; + return element_size; +} + +bool Cluster::PreWriteBlock() { + if (finalized_) + return false; + + if (!header_written_) { + if (!WriteClusterHeader()) + return false; + } + + return true; +} + +void Cluster::PostWriteBlock(uint64_t element_size) { + AddPayloadSize(element_size); + ++blocks_added_; +} + +int64_t Cluster::GetRelativeTimecode(int64_t abs_timecode) const { + const int64_t cluster_timecode = this->Cluster::timecode(); + const int64_t rel_timecode = + static_cast(abs_timecode) - cluster_timecode; + + if (rel_timecode < 0 || rel_timecode > kMaxBlockTimecode) + return -1; + + return rel_timecode; +} + +bool Cluster::DoWriteFrame(const Frame* const frame) { + if (!frame || !frame->IsValid()) + return false; + + if (!PreWriteBlock()) + return false; + + const uint64_t element_size = WriteFrame(writer_, frame, this); + if (element_size == 0) + return false; + + PostWriteBlock(element_size); + last_block_timestamp_[frame->track_number()] = frame->timestamp(); + return true; +} + +bool Cluster::QueueOrWriteFrame(const Frame* const frame) { + if (!frame || !frame->IsValid()) + return false; + + // If |write_last_frame_with_duration_| is not set, then write the frame right + // away. + if (!write_last_frame_with_duration_) { + return DoWriteFrame(frame); + } + + // Queue the current frame. + uint64_t track_number = frame->track_number(); + Frame* const frame_to_store = new Frame(); + frame_to_store->CopyFrom(*frame); + stored_frames_[track_number].push_back(frame_to_store); + + // Iterate through all queued frames in the current track except the last one + // and write it if it is okay to do so (i.e.) no other track has an held back + // frame with timestamp <= the timestamp of the frame in question. + std::vector::iterator> frames_to_erase; + for (std::list::iterator + current_track_iterator = stored_frames_[track_number].begin(), + end = --stored_frames_[track_number].end(); + current_track_iterator != end; ++current_track_iterator) { + const Frame* const frame_to_write = *current_track_iterator; + bool okay_to_write = true; + for (FrameMapIterator track_iterator = stored_frames_.begin(); + track_iterator != stored_frames_.end(); ++track_iterator) { + if (track_iterator->first == track_number) { + continue; + } + if (track_iterator->second.front()->timestamp() < + frame_to_write->timestamp()) { + okay_to_write = false; + break; + } + } + if (okay_to_write) { + const bool wrote_frame = DoWriteFrame(frame_to_write); + delete frame_to_write; + if (!wrote_frame) + return false; + frames_to_erase.push_back(current_track_iterator); + } else { + break; + } + } + for (std::vector::iterator>::iterator iterator = + frames_to_erase.begin(); + iterator != frames_to_erase.end(); ++iterator) { + stored_frames_[track_number].erase(*iterator); + } + return true; +} + +bool Cluster::WriteClusterHeader() { + if (finalized_) + return false; + + if (WriteID(writer_, libwebm::kMkvCluster)) + return false; + + // Save for later. + size_position_ = writer_->Position(); + + // Write "unknown" (EBML coded -1) as cluster size value. We need to write 8 + // bytes because we do not know how big our cluster will be. + if (SerializeInt(writer_, kEbmlUnknownValue, 8)) + return false; + + if (!WriteEbmlElement(writer_, libwebm::kMkvTimecode, timecode(), + fixed_size_timecode_ ? 8 : 0)) { + return false; + } + AddPayloadSize(EbmlElementSize(libwebm::kMkvTimecode, timecode(), + fixed_size_timecode_ ? 8 : 0)); + header_written_ = true; + + return true; +} + +/////////////////////////////////////////////////////////////// +// +// SeekHead Class + +SeekHead::SeekHead() : start_pos_(0ULL) { + for (int32_t i = 0; i < kSeekEntryCount; ++i) { + seek_entry_id_[i] = 0; + seek_entry_pos_[i] = 0; + } +} + +SeekHead::~SeekHead() {} + +bool SeekHead::Finalize(IMkvWriter* writer) const { + if (writer->Seekable()) { + if (start_pos_ == -1) + return false; + + uint64_t payload_size = 0; + uint64_t entry_size[kSeekEntryCount]; + + for (int32_t i = 0; i < kSeekEntryCount; ++i) { + if (seek_entry_id_[i] != 0) { + entry_size[i] = EbmlElementSize(libwebm::kMkvSeekID, + static_cast(seek_entry_id_[i])); + entry_size[i] += EbmlElementSize( + libwebm::kMkvSeekPosition, static_cast(seek_entry_pos_[i])); + + payload_size += + EbmlMasterElementSize(libwebm::kMkvSeek, entry_size[i]) + + entry_size[i]; + } + } + + // No SeekHead elements + if (payload_size == 0) + return true; + + const int64_t pos = writer->Position(); + if (writer->Position(start_pos_)) + return false; + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvSeekHead, payload_size)) + return false; + + for (int32_t i = 0; i < kSeekEntryCount; ++i) { + if (seek_entry_id_[i] != 0) { + if (!WriteEbmlMasterElement(writer, libwebm::kMkvSeek, entry_size[i])) + return false; + + if (!WriteEbmlElement(writer, libwebm::kMkvSeekID, + static_cast(seek_entry_id_[i]))) + return false; + + if (!WriteEbmlElement(writer, libwebm::kMkvSeekPosition, + static_cast(seek_entry_pos_[i]))) + return false; + } + } + + const uint64_t total_entry_size = kSeekEntryCount * MaxEntrySize(); + const uint64_t total_size = + EbmlMasterElementSize(libwebm::kMkvSeekHead, total_entry_size) + + total_entry_size; + const int64_t size_left = total_size - (writer->Position() - start_pos_); + + const uint64_t bytes_written = WriteVoidElement(writer, size_left); + if (!bytes_written) + return false; + + if (writer->Position(pos)) + return false; + } + + return true; +} + +bool SeekHead::Write(IMkvWriter* writer) { + const uint64_t entry_size = kSeekEntryCount * MaxEntrySize(); + const uint64_t size = + EbmlMasterElementSize(libwebm::kMkvSeekHead, entry_size); + + start_pos_ = writer->Position(); + + const uint64_t bytes_written = WriteVoidElement(writer, size + entry_size); + if (!bytes_written) + return false; + + return true; +} + +bool SeekHead::AddSeekEntry(uint32_t id, uint64_t pos) { + for (int32_t i = 0; i < kSeekEntryCount; ++i) { + if (seek_entry_id_[i] == 0) { + seek_entry_id_[i] = id; + seek_entry_pos_[i] = pos; + return true; + } + } + return false; +} + +uint32_t SeekHead::GetId(int index) const { + if (index < 0 || index >= kSeekEntryCount) + return UINT_MAX; + return seek_entry_id_[index]; +} + +uint64_t SeekHead::GetPosition(int index) const { + if (index < 0 || index >= kSeekEntryCount) + return ULLONG_MAX; + return seek_entry_pos_[index]; +} + +bool SeekHead::SetSeekEntry(int index, uint32_t id, uint64_t position) { + if (index < 0 || index >= kSeekEntryCount) + return false; + seek_entry_id_[index] = id; + seek_entry_pos_[index] = position; + return true; +} + +uint64_t SeekHead::MaxEntrySize() const { + const uint64_t max_entry_payload_size = + EbmlElementSize(libwebm::kMkvSeekID, + static_cast(UINT64_C(0xffffffff))) + + EbmlElementSize(libwebm::kMkvSeekPosition, + static_cast(UINT64_C(0xffffffffffffffff))); + const uint64_t max_entry_size = + EbmlMasterElementSize(libwebm::kMkvSeek, max_entry_payload_size) + + max_entry_payload_size; + + return max_entry_size; +} + +/////////////////////////////////////////////////////////////// +// +// SegmentInfo Class + +SegmentInfo::SegmentInfo() + : duration_(-1.0), + muxing_app_(NULL), + timecode_scale_(1000000ULL), + writing_app_(NULL), + date_utc_(LLONG_MIN), + duration_pos_(-1) {} + +SegmentInfo::~SegmentInfo() { + delete[] muxing_app_; + delete[] writing_app_; +} + +bool SegmentInfo::Init() { + int32_t major; + int32_t minor; + int32_t build; + int32_t revision; + GetVersion(&major, &minor, &build, &revision); + char temp[256]; +#ifdef _MSC_VER + sprintf_s(temp, sizeof(temp) / sizeof(temp[0]), "libwebm-%d.%d.%d.%d", major, + minor, build, revision); +#else + snprintf(temp, sizeof(temp) / sizeof(temp[0]), "libwebm-%d.%d.%d.%d", major, + minor, build, revision); +#endif + + const size_t app_len = strlen(temp) + 1; + + delete[] muxing_app_; + + muxing_app_ = new (std::nothrow) char[app_len]; // NOLINT + if (!muxing_app_) + return false; + +#ifdef _MSC_VER + strcpy_s(muxing_app_, app_len, temp); +#else + strcpy(muxing_app_, temp); +#endif + + set_writing_app(temp); + if (!writing_app_) + return false; + return true; +} + +bool SegmentInfo::Finalize(IMkvWriter* writer) const { + if (!writer) + return false; + + if (duration_ > 0.0) { + if (writer->Seekable()) { + if (duration_pos_ == -1) + return false; + + const int64_t pos = writer->Position(); + + if (writer->Position(duration_pos_)) + return false; + + if (!WriteEbmlElement(writer, libwebm::kMkvDuration, + static_cast(duration_))) + return false; + + if (writer->Position(pos)) + return false; + } + } + + return true; +} + +bool SegmentInfo::Write(IMkvWriter* writer) { + if (!writer || !muxing_app_ || !writing_app_) + return false; + + uint64_t size = EbmlElementSize(libwebm::kMkvTimecodeScale, + static_cast(timecode_scale_)); + if (duration_ > 0.0) + size += + EbmlElementSize(libwebm::kMkvDuration, static_cast(duration_)); + if (date_utc_ != LLONG_MIN) + size += EbmlDateElementSize(libwebm::kMkvDateUTC); + size += EbmlElementSize(libwebm::kMkvMuxingApp, muxing_app_); + size += EbmlElementSize(libwebm::kMkvWritingApp, writing_app_); + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvInfo, size)) + return false; + + const int64_t payload_position = writer->Position(); + if (payload_position < 0) + return false; + + if (!WriteEbmlElement(writer, libwebm::kMkvTimecodeScale, + static_cast(timecode_scale_))) + return false; + + if (duration_ > 0.0) { + // Save for later + duration_pos_ = writer->Position(); + + if (!WriteEbmlElement(writer, libwebm::kMkvDuration, + static_cast(duration_))) + return false; + } + + if (date_utc_ != LLONG_MIN) + WriteEbmlDateElement(writer, libwebm::kMkvDateUTC, date_utc_); + + if (!WriteEbmlElement(writer, libwebm::kMkvMuxingApp, muxing_app_)) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvWritingApp, writing_app_)) + return false; + + const int64_t stop_position = writer->Position(); + if (stop_position < 0 || + stop_position - payload_position != static_cast(size)) + return false; + + return true; +} + +void SegmentInfo::set_muxing_app(const char* app) { + if (app) { + const size_t length = strlen(app) + 1; + char* temp_str = new (std::nothrow) char[length]; // NOLINT + if (!temp_str) + return; + +#ifdef _MSC_VER + strcpy_s(temp_str, length, app); +#else + strcpy(temp_str, app); +#endif + + delete[] muxing_app_; + muxing_app_ = temp_str; + } +} + +void SegmentInfo::set_writing_app(const char* app) { + if (app) { + const size_t length = strlen(app) + 1; + char* temp_str = new (std::nothrow) char[length]; // NOLINT + if (!temp_str) + return; + +#ifdef _MSC_VER + strcpy_s(temp_str, length, app); +#else + strcpy(temp_str, app); +#endif + + delete[] writing_app_; + writing_app_ = temp_str; + } +} + +/////////////////////////////////////////////////////////////// +// +// Segment Class + +Segment::Segment() + : + cluster_list_(NULL), + cluster_list_capacity_(0), + cluster_list_size_(0), + cues_position_(kAfterClusters), + cues_track_(0), + force_new_cluster_(false), + frames_(NULL), + frames_capacity_(0), + frames_size_(0), + has_video_(false), + header_written_(false), + last_block_duration_(0), + last_timestamp_(0), + max_cluster_duration_(kDefaultMaxClusterDuration), + max_cluster_size_(0), + mode_(kFile), + new_cuepoint_(false), + output_cues_(true), + accurate_cluster_duration_(false), + fixed_size_cluster_timecode_(false), + payload_pos_(0), + size_position_(0), + doc_type_version_(kDefaultDocTypeVersion), + doc_type_version_written_(0), + writer_cluster_(NULL), + writer_cues_(NULL), + writer_header_(NULL) { + const time_t curr_time = time(NULL); + seed_ = static_cast(curr_time); +#ifdef _WIN32 + srand(seed_); +#endif +} + +Segment::~Segment() { + if (cluster_list_) { + for (int32_t i = 0; i < cluster_list_size_; ++i) { + Cluster* const cluster = cluster_list_[i]; + delete cluster; + } + delete[] cluster_list_; + } + + if (frames_) { + for (int32_t i = 0; i < frames_size_; ++i) { + Frame* const frame = frames_[i]; + delete frame; + } + delete[] frames_; + } +} + +void Segment::MoveCuesBeforeClustersHelper(uint64_t diff, int32_t index, + uint64_t* cues_size) { + CuePoint* const cue_point = cues_.GetCueByIndex(index); + if (cue_point == NULL) + return; + const uint64_t old_cue_point_size = cue_point->Size(); + const uint64_t cluster_pos = cue_point->cluster_pos() + diff; + cue_point->set_cluster_pos(cluster_pos); // update the new cluster position + // New size of the cue is computed as follows + // Let a = current sum of size of all CuePoints + // Let b = Increase in Cue Point's size due to this iteration + // Let c = Increase in size of Cues Element's length due to this iteration + // (This is computed as CodedSize(a + b) - CodedSize(a)) + // Let d = b + c. Now d is the |diff| passed to the next recursive call. + // Let e = a + b. Now e is the |cues_size| passed to the next recursive + // call. + const uint64_t cue_point_size_diff = cue_point->Size() - old_cue_point_size; + const uint64_t cue_size_diff = + GetCodedUIntSize(*cues_size + cue_point_size_diff) - + GetCodedUIntSize(*cues_size); + *cues_size += cue_point_size_diff; + diff = cue_size_diff + cue_point_size_diff; + if (diff > 0) { + for (int32_t i = 0; i < cues_.cue_entries_size(); ++i) { + MoveCuesBeforeClustersHelper(diff, i, cues_size); + } + } +} + +void Segment::MoveCuesBeforeClusters() { + const uint64_t current_cue_size = cues_.Size(); + uint64_t cue_size = 0; + for (int32_t i = 0; i < cues_.cue_entries_size(); ++i) + cue_size += cues_.GetCueByIndex(i)->Size(); + for (int32_t i = 0; i < cues_.cue_entries_size(); ++i) + MoveCuesBeforeClustersHelper(current_cue_size, i, &cue_size); + + // Adjust the Seek Entry to reflect the change in position + // of Cluster and Cues + int32_t cluster_index = 0; + int32_t cues_index = 0; + for (int32_t i = 0; i < SeekHead::kSeekEntryCount; ++i) { + if (seek_head_.GetId(i) == libwebm::kMkvCluster) + cluster_index = i; + if (seek_head_.GetId(i) == libwebm::kMkvCues) + cues_index = i; + } + seek_head_.SetSeekEntry(cues_index, libwebm::kMkvCues, + seek_head_.GetPosition(cluster_index)); + seek_head_.SetSeekEntry(cluster_index, libwebm::kMkvCluster, + cues_.Size() + seek_head_.GetPosition(cues_index)); +} + +bool Segment::Init(IMkvWriter* ptr_writer) { + if (!ptr_writer) { + return false; + } + writer_cluster_ = ptr_writer; + writer_cues_ = ptr_writer; + writer_header_ = ptr_writer; + return segment_info_.Init(); +} + +bool Segment::Finalize() { + if (WriteFramesAll() < 0) + return false; + + // In kLive mode, call Cluster::Finalize only if |accurate_cluster_duration_| + // is set. In all other modes, always call Cluster::Finalize. + if ((mode_ == kLive ? accurate_cluster_duration_ : true) && + cluster_list_size_ > 0) { + // Update last cluster's size + Cluster* const old_cluster = cluster_list_[cluster_list_size_ - 1]; + + // For the last frame of the last Cluster, we don't write it as a BlockGroup + // with Duration unless the frame itself has duration set explicitly. + if (!old_cluster || !old_cluster->Finalize(false, 0)) + return false; + } + + if (mode_ == kFile) { + + const double duration = + (static_cast(last_timestamp_) + last_block_duration_) / + segment_info_.timecode_scale(); + segment_info_.set_duration(duration); + if (!segment_info_.Finalize(writer_header_)) + return false; + + if (output_cues_) + if (!seek_head_.AddSeekEntry(libwebm::kMkvCues, MaxOffset())) + return false; + + cluster_end_offset_ = writer_cluster_->Position(); + + // Write the seek headers and cues + if (output_cues_) + if (!cues_.Write(writer_cues_)) + return false; + + if (!seek_head_.Finalize(writer_header_)) + return false; + + if (writer_header_->Seekable()) { + if (size_position_ == -1) + return false; + + const int64_t segment_size = MaxOffset(); + if (segment_size < 1) + return false; + + const int64_t pos = writer_header_->Position(); + UpdateDocTypeVersion(); + if (doc_type_version_ != doc_type_version_written_) { + if (writer_header_->Position(0)) + return false; + + const char* const doc_type = + DocTypeIsWebm() ? kDocTypeWebm : kDocTypeMatroska; + if (!WriteEbmlHeader(writer_header_, doc_type_version_, doc_type)) + return false; + if (writer_header_->Position() != ebml_header_size_) + return false; + + doc_type_version_written_ = doc_type_version_; + } + + if (writer_header_->Position(size_position_)) + return false; + + if (WriteUIntSize(writer_header_, segment_size, 8)) + return false; + + if (writer_header_->Position(pos)) + return false; + } + } + + return true; +} + +Track* Segment::AddTrack(int32_t number) { + Track* const track = new (std::nothrow) Track(&seed_); // NOLINT + + if (!track) + return NULL; + + if (!tracks_.AddTrack(track, number)) { + delete track; + return NULL; + } + + return track; +} + +Chapter* Segment::AddChapter() { return chapters_.AddChapter(&seed_); } + +Tag* Segment::AddTag() { return tags_.AddTag(); } + +uint64_t Segment::AddVideoTrack(int32_t width, int32_t height, int32_t number) { + VideoTrack* const track = new (std::nothrow) VideoTrack(&seed_); // NOLINT + if (!track) + return 0; + + track->set_type(Tracks::kVideo); + track->set_codec_id(Tracks::kVp8CodecId); + track->set_width(width); + track->set_height(height); + + tracks_.AddTrack(track, number); + has_video_ = true; + + return track->number(); +} + +bool Segment::AddCuePoint(uint64_t timestamp, uint64_t track) { + if (cluster_list_size_ < 1) + return false; + + const Cluster* const cluster = cluster_list_[cluster_list_size_ - 1]; + if (!cluster) + return false; + + CuePoint* const cue = new (std::nothrow) CuePoint(); // NOLINT + if (!cue) + return false; + + cue->set_time(timestamp / segment_info_.timecode_scale()); + cue->set_block_number(cluster->blocks_added()); + cue->set_cluster_pos(cluster->position_for_cues()); + cue->set_track(track); + if (!cues_.AddCue(cue)) + return false; + + new_cuepoint_ = false; + return true; +} + +uint64_t Segment::AddAudioTrack(int32_t sample_rate, int32_t channels, + int32_t number) { + AudioTrack* const track = new (std::nothrow) AudioTrack(&seed_); // NOLINT + if (!track) + return 0; + + track->set_type(Tracks::kAudio); + track->set_codec_id(Tracks::kVorbisCodecId); + track->set_sample_rate(sample_rate); + track->set_channels(channels); + + tracks_.AddTrack(track, number); + + return track->number(); +} + +bool Segment::AddFrame(const uint8_t* data, uint64_t length, + uint64_t track_number, uint64_t timestamp, bool is_key) { + if (!data) + return false; + + Frame frame; + if (!frame.Init(data, length)) + return false; + frame.set_track_number(track_number); + frame.set_timestamp(timestamp); + frame.set_is_key(is_key); + return AddGenericFrame(&frame); +} + +bool Segment::AddFrameWithAdditional(const uint8_t* data, uint64_t length, + const uint8_t* additional, + uint64_t additional_length, + uint64_t add_id, uint64_t track_number, + uint64_t timestamp, bool is_key) { + if (!data || !additional) + return false; + + Frame frame; + if (!frame.Init(data, length) || + !frame.AddAdditionalData(additional, additional_length, add_id)) { + return false; + } + frame.set_track_number(track_number); + frame.set_timestamp(timestamp); + frame.set_is_key(is_key); + return AddGenericFrame(&frame); +} + +bool Segment::AddFrameWithDiscardPadding(const uint8_t* data, uint64_t length, + int64_t discard_padding, + uint64_t track_number, + uint64_t timestamp, bool is_key) { + if (!data) + return false; + + Frame frame; + if (!frame.Init(data, length)) + return false; + frame.set_discard_padding(discard_padding); + frame.set_track_number(track_number); + frame.set_timestamp(timestamp); + frame.set_is_key(is_key); + return AddGenericFrame(&frame); +} + +bool Segment::AddMetadata(const uint8_t* data, uint64_t length, + uint64_t track_number, uint64_t timestamp_ns, + uint64_t duration_ns) { + if (!data) + return false; + + Frame frame; + if (!frame.Init(data, length)) + return false; + frame.set_track_number(track_number); + frame.set_timestamp(timestamp_ns); + frame.set_duration(duration_ns); + frame.set_is_key(true); // All metadata blocks are keyframes. + return AddGenericFrame(&frame); +} + +bool Segment::AddGenericFrame(const Frame* frame) { + if (!frame) + return false; + + if (!CheckHeaderInfo()) + return false; + + // Check for non-monotonically increasing timestamps. + if (frame->timestamp() < last_timestamp_) + return false; + + // Check if the track number is valid. + if (!tracks_.GetTrackByNumber(frame->track_number())) + return false; + + if (frame->discard_padding() != 0) + doc_type_version_ = 4; + + // If the segment has a video track hold onto audio frames to make sure the + // audio that is associated with the start time of a video key-frame is + // muxed into the same cluster. + if (has_video_ && tracks_.TrackIsAudio(frame->track_number()) && + !force_new_cluster_) { + Frame* const new_frame = new (std::nothrow) Frame(); + if (!new_frame || !new_frame->CopyFrom(*frame)) + return false; + return QueueFrame(new_frame); + } + + if (!DoNewClusterProcessing(frame->track_number(), frame->timestamp(), + frame->is_key())) { + return false; + } + + if (cluster_list_size_ < 1) + return false; + + Cluster* const cluster = cluster_list_[cluster_list_size_ - 1]; + if (!cluster) + return false; + + // If the Frame is not a SimpleBlock, then set the reference_block_timestamp + // if it is not set already. + bool frame_created = false; + if (!frame->CanBeSimpleBlock() && !frame->is_key() && + !frame->reference_block_timestamp_set()) { + Frame* const new_frame = new (std::nothrow) Frame(); + if (!new_frame->CopyFrom(*frame)) + return false; + new_frame->set_reference_block_timestamp( + last_track_timestamp_[frame->track_number() - 1]); + frame = new_frame; + frame_created = true; + } + + if (!cluster->AddFrame(frame)) + return false; + + if (new_cuepoint_ && cues_track_ == frame->track_number()) { + if (!AddCuePoint(frame->timestamp(), cues_track_)) + return false; + } + + last_timestamp_ = frame->timestamp(); + last_track_timestamp_[frame->track_number() - 1] = frame->timestamp(); + last_block_duration_ = frame->duration(); + + if (frame_created) + delete frame; + + return true; +} + +void Segment::OutputCues(bool output_cues) { output_cues_ = output_cues; } + +void Segment::AccurateClusterDuration(bool accurate_cluster_duration) { + accurate_cluster_duration_ = accurate_cluster_duration; +} + +void Segment::UseFixedSizeClusterTimecode(bool fixed_size_cluster_timecode) { + fixed_size_cluster_timecode_ = fixed_size_cluster_timecode; +} + +bool Segment::CuesTrack(uint64_t track_number) { + const Track* const track = GetTrackByNumber(track_number); + if (!track) + return false; + + cues_track_ = track_number; + return true; +} + +void Segment::ForceNewClusterOnNextFrame() { force_new_cluster_ = true; } + +Track* Segment::GetTrackByNumber(uint64_t track_number) const { + return tracks_.GetTrackByNumber(track_number); +} + +bool Segment::WriteSegmentHeader() { + UpdateDocTypeVersion(); + + const char* const doc_type = + DocTypeIsWebm() ? kDocTypeWebm : kDocTypeMatroska; + if (!WriteEbmlHeader(writer_header_, doc_type_version_, doc_type)) + return false; + doc_type_version_written_ = doc_type_version_; + ebml_header_size_ = static_cast(writer_header_->Position()); + + // Write "unknown" (-1) as segment size value. If mode is kFile, Segment + // will write over duration when the file is finalized. + if (WriteID(writer_header_, libwebm::kMkvSegment)) + return false; + + // Save for later. + size_position_ = writer_header_->Position(); + + // Write "unknown" (EBML coded -1) as segment size value. We need to write 8 + // bytes because if we are going to overwrite the segment size later we do + // not know how big our segment will be. + if (SerializeInt(writer_header_, kEbmlUnknownValue, 8)) + return false; + + payload_pos_ = writer_header_->Position(); + + if (mode_ == kFile && writer_header_->Seekable()) { + // Set the duration > 0.0 so SegmentInfo will write out the duration. When + // the muxer is done writing we will set the correct duration and have + // SegmentInfo upadte it. + segment_info_.set_duration(1.0); + + if (!seek_head_.Write(writer_header_)) + return false; + } + + if (!seek_head_.AddSeekEntry(libwebm::kMkvInfo, MaxOffset())) + return false; + if (!segment_info_.Write(writer_header_)) + return false; + + if (!seek_head_.AddSeekEntry(libwebm::kMkvTracks, MaxOffset())) + return false; + if (!tracks_.Write(writer_header_)) + return false; + + if (chapters_.Count() > 0) { + if (!seek_head_.AddSeekEntry(libwebm::kMkvChapters, MaxOffset())) + return false; + if (!chapters_.Write(writer_header_)) + return false; + } + + if (tags_.Count() > 0) { + if (!seek_head_.AddSeekEntry(libwebm::kMkvTags, MaxOffset())) + return false; + if (!tags_.Write(writer_header_)) + return false; + } + + header_written_ = true; + + return true; +} + +// Here we are testing whether to create a new cluster, given a frame +// having time frame_timestamp_ns. +// +int Segment::TestFrame(uint64_t track_number, uint64_t frame_timestamp_ns, + bool is_key) const { + if (force_new_cluster_) + return 1; + + // If no clusters have been created yet, then create a new cluster + // and write this frame immediately, in the new cluster. This path + // should only be followed once, the first time we attempt to write + // a frame. + + if (cluster_list_size_ <= 0) + return 1; + + // There exists at least one cluster. We must compare the frame to + // the last cluster, in order to determine whether the frame is + // written to the existing cluster, or that a new cluster should be + // created. + + const uint64_t timecode_scale = segment_info_.timecode_scale(); + const uint64_t frame_timecode = frame_timestamp_ns / timecode_scale; + + const Cluster* const last_cluster = cluster_list_[cluster_list_size_ - 1]; + const uint64_t last_cluster_timecode = last_cluster->timecode(); + + // For completeness we test for the case when the frame's timecode + // is less than the cluster's timecode. Although in principle that + // is allowed, this muxer doesn't actually write clusters like that, + // so this indicates a bug somewhere in our algorithm. + + if (frame_timecode < last_cluster_timecode) // should never happen + return -1; + + // If the frame has a timestamp significantly larger than the last + // cluster (in Matroska, cluster-relative timestamps are serialized + // using a 16-bit signed integer), then we cannot write this frame + // to that cluster, and so we must create a new cluster. + + const int64_t delta_timecode = frame_timecode - last_cluster_timecode; + + if (delta_timecode > kMaxBlockTimecode) + return 2; + + // We decide to create a new cluster when we have a video keyframe. + // This will flush queued (audio) frames, and write the keyframe + // immediately, in the newly-created cluster. + + if (is_key && tracks_.TrackIsVideo(track_number)) + return 1; + + // Create a new cluster if we have accumulated too many frames + // already, where "too many" is defined as "the total time of frames + // in the cluster exceeds a threshold". + + const uint64_t delta_ns = delta_timecode * timecode_scale; + + if (max_cluster_duration_ > 0 && delta_ns >= max_cluster_duration_) + return 1; + + // This is similar to the case above, with the difference that a new + // cluster is created when the size of the current cluster exceeds a + // threshold. + + const uint64_t cluster_size = last_cluster->payload_size(); + + if (max_cluster_size_ > 0 && cluster_size >= max_cluster_size_) + return 1; + + // There's no need to create a new cluster, so emit this frame now. + + return 0; +} + +bool Segment::MakeNewCluster(uint64_t frame_timestamp_ns) { + const int32_t new_size = cluster_list_size_ + 1; + + if (new_size > cluster_list_capacity_) { + // Add more clusters. + const int32_t new_capacity = + (cluster_list_capacity_ <= 0) ? 1 : cluster_list_capacity_ * 2; + Cluster** const clusters = + new (std::nothrow) Cluster*[new_capacity]; // NOLINT + if (!clusters) + return false; + + for (int32_t i = 0; i < cluster_list_size_; ++i) { + clusters[i] = cluster_list_[i]; + } + + delete[] cluster_list_; + + cluster_list_ = clusters; + cluster_list_capacity_ = new_capacity; + } + + if (!WriteFramesLessThan(frame_timestamp_ns)) + return false; + + if (cluster_list_size_ > 0) { + // Update old cluster's size + Cluster* const old_cluster = cluster_list_[cluster_list_size_ - 1]; + + if (!old_cluster || !old_cluster->Finalize(true, frame_timestamp_ns)) + return false; + } + + if (output_cues_) + new_cuepoint_ = true; + + const uint64_t timecode_scale = segment_info_.timecode_scale(); + const uint64_t frame_timecode = frame_timestamp_ns / timecode_scale; + + uint64_t cluster_timecode = frame_timecode; + + if (frames_size_ > 0) { + const Frame* const f = frames_[0]; // earliest queued frame + const uint64_t ns = f->timestamp(); + const uint64_t tc = ns / timecode_scale; + + if (tc < cluster_timecode) + cluster_timecode = tc; + } + + Cluster*& cluster = cluster_list_[cluster_list_size_]; + const int64_t offset = MaxOffset(); + cluster = new (std::nothrow) + Cluster(cluster_timecode, offset, segment_info_.timecode_scale(), + accurate_cluster_duration_, fixed_size_cluster_timecode_); + if (!cluster) + return false; + + if (!cluster->Init(writer_cluster_)) + return false; + + cluster_list_size_ = new_size; + return true; +} + +bool Segment::DoNewClusterProcessing(uint64_t track_number, + uint64_t frame_timestamp_ns, bool is_key) { + for (;;) { + // Based on the characteristics of the current frame and current + // cluster, decide whether to create a new cluster. + const int result = TestFrame(track_number, frame_timestamp_ns, is_key); + if (result < 0) // error + return false; + + // Always set force_new_cluster_ to false after TestFrame. + force_new_cluster_ = false; + + // A non-zero result means create a new cluster. + if (result > 0 && !MakeNewCluster(frame_timestamp_ns)) + return false; + + // Write queued (audio) frames. + const int frame_count = WriteFramesAll(); + if (frame_count < 0) // error + return false; + + // Write the current frame to the current cluster (if TestFrame + // returns 0) or to a newly created cluster (TestFrame returns 1). + if (result <= 1) + return true; + + // TestFrame returned 2, which means there was a large time + // difference between the cluster and the frame itself. Do the + // test again, comparing the frame to the new cluster. + } +} + +bool Segment::CheckHeaderInfo() { + if (!header_written_) { + if (!WriteSegmentHeader()) + return false; + + if (!seek_head_.AddSeekEntry(libwebm::kMkvCluster, MaxOffset())) + return false; + + if (output_cues_ && cues_track_ == 0) { + // Check for a video track + for (uint32_t i = 0; i < tracks_.track_entries_size(); ++i) { + const Track* const track = tracks_.GetTrackByIndex(i); + if (!track) + return false; + + if (tracks_.TrackIsVideo(track->number())) { + cues_track_ = track->number(); + break; + } + } + + // Set first track found + if (cues_track_ == 0) { + const Track* const track = tracks_.GetTrackByIndex(0); + if (!track) + return false; + + cues_track_ = track->number(); + } + } + } + return true; +} + +void Segment::UpdateDocTypeVersion() { + for (uint32_t index = 0; index < tracks_.track_entries_size(); ++index) { + const Track* track = tracks_.GetTrackByIndex(index); + if (track == NULL) + break; + if ((track->codec_delay() || track->seek_pre_roll()) && + doc_type_version_ < 4) { + doc_type_version_ = 4; + break; + } + } +} + +int64_t Segment::MaxOffset() { + if (!writer_header_) + return -1; + + int64_t offset = writer_header_->Position() - payload_pos_; + + return offset; +} + +bool Segment::QueueFrame(Frame* frame) { + const int32_t new_size = frames_size_ + 1; + + if (new_size > frames_capacity_) { + // Add more frames. + const int32_t new_capacity = (!frames_capacity_) ? 2 : frames_capacity_ * 2; + + if (new_capacity < 1) + return false; + + Frame** const frames = new (std::nothrow) Frame*[new_capacity]; // NOLINT + if (!frames) + return false; + + for (int32_t i = 0; i < frames_size_; ++i) { + frames[i] = frames_[i]; + } + + delete[] frames_; + frames_ = frames; + frames_capacity_ = new_capacity; + } + + frames_[frames_size_++] = frame; + + return true; +} + +int Segment::WriteFramesAll() { + if (frames_ == NULL) + return 0; + + if (cluster_list_size_ < 1) + return -1; + + Cluster* const cluster = cluster_list_[cluster_list_size_ - 1]; + + if (!cluster) + return -1; + + for (int32_t i = 0; i < frames_size_; ++i) { + Frame*& frame = frames_[i]; + // TODO(jzern/vigneshv): using Segment::AddGenericFrame here would limit the + // places where |doc_type_version_| needs to be updated. + if (frame->discard_padding() != 0) + doc_type_version_ = 4; + if (!cluster->AddFrame(frame)) + return -1; + + if (new_cuepoint_ && cues_track_ == frame->track_number()) { + if (!AddCuePoint(frame->timestamp(), cues_track_)) + return -1; + } + + if (frame->timestamp() > last_timestamp_) { + last_timestamp_ = frame->timestamp(); + last_track_timestamp_[frame->track_number() - 1] = frame->timestamp(); + } + + delete frame; + frame = NULL; + } + + const int result = frames_size_; + frames_size_ = 0; + + return result; +} + +bool Segment::WriteFramesLessThan(uint64_t timestamp) { + // Check |cluster_list_size_| to see if this is the first cluster. If it is + // the first cluster the audio frames that are less than the first video + // timesatmp will be written in a later step. + if (frames_size_ > 0 && cluster_list_size_ > 0) { + if (!frames_) + return false; + + Cluster* const cluster = cluster_list_[cluster_list_size_ - 1]; + if (!cluster) + return false; + + int32_t shift_left = 0; + + // TODO(fgalligan): Change this to use the durations of frames instead of + // the next frame's start time if the duration is accurate. + for (int32_t i = 1; i < frames_size_; ++i) { + const Frame* const frame_curr = frames_[i]; + + if (frame_curr->timestamp() > timestamp) + break; + + const Frame* const frame_prev = frames_[i - 1]; + if (frame_prev->discard_padding() != 0) + doc_type_version_ = 4; + if (!cluster->AddFrame(frame_prev)) + return false; + + if (new_cuepoint_ && cues_track_ == frame_prev->track_number()) { + if (!AddCuePoint(frame_prev->timestamp(), cues_track_)) + return false; + } + + ++shift_left; + if (frame_prev->timestamp() > last_timestamp_) { + last_timestamp_ = frame_prev->timestamp(); + last_track_timestamp_[frame_prev->track_number() - 1] = + frame_prev->timestamp(); + } + + delete frame_prev; + } + + if (shift_left > 0) { + if (shift_left >= frames_size_) + return false; + + const int32_t new_frames_size = frames_size_ - shift_left; + for (int32_t i = 0; i < new_frames_size; ++i) { + frames_[i] = frames_[i + shift_left]; + } + + frames_size_ = new_frames_size; + } + } + + return true; +} + +bool Segment::DocTypeIsWebm() const { + const int kNumCodecIds = 9; + + // TODO(vigneshv): Tweak .clang-format. + const char* kWebmCodecIds[kNumCodecIds] = { + Tracks::kOpusCodecId, Tracks::kVorbisCodecId, + Tracks::kVp8CodecId, Tracks::kVp9CodecId, + Tracks::kVp10CodecId, Tracks::kWebVttCaptionsId, + Tracks::kWebVttDescriptionsId, Tracks::kWebVttMetadataId, + Tracks::kWebVttSubtitlesId}; + + const int num_tracks = static_cast(tracks_.track_entries_size()); + for (int track_index = 0; track_index < num_tracks; ++track_index) { + const Track* const track = tracks_.GetTrackByIndex(track_index); + const std::string codec_id = track->codec_id(); + + bool id_is_webm = false; + for (int id_index = 0; id_index < kNumCodecIds; ++id_index) { + if (codec_id == kWebmCodecIds[id_index]) { + id_is_webm = true; + break; + } + } + + if (!id_is_webm) + return false; + } + + return true; +} + +} // namespace mkvmuxer diff --git a/src/client/bundled/mkvmuxer/mkvmuxer.h b/src/client/bundled/mkvmuxer/mkvmuxer.h new file mode 100644 index 000000000..ac1aad053 --- /dev/null +++ b/src/client/bundled/mkvmuxer/mkvmuxer.h @@ -0,0 +1,1656 @@ +// Copyright (c) 2012 The WebM project authors. All Rights Reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. An additional intellectual property rights grant can be found +// in the file PATENTS. All contributing project authors may +// be found in the AUTHORS file in the root of the source tree. + +#ifndef MKVMUXER_MKVMUXER_H_ +#define MKVMUXER_MKVMUXER_H_ + +#include + +#include +#include +#include + +#include "common/webmids.h" +#include "mkvmuxer/mkvmuxertypes.h" + +// For a description of the WebM elements see +// http://www.webmproject.org/code/specs/container/. + +namespace mkvparser { +class IMkvReader; +} // namespace mkvparser + +namespace mkvmuxer { + +class Segment; + +const uint64_t kMaxTrackNumber = 126; + +/////////////////////////////////////////////////////////////// +// Interface used by the mkvmuxer to write out the Mkv data. +class IMkvWriter { + public: + // Writes out |len| bytes of |buf|. Returns 0 on success. + virtual int32 Write(const void* buf, uint32 len) = 0; + + // Returns the offset of the output position from the beginning of the + // output. + virtual int64 Position() const = 0; + + // Set the current File position. Returns 0 on success. + virtual int32 Position(int64 position) = 0; + + // Returns true if the writer is seekable. + virtual bool Seekable() const = 0; + + // Element start notification. Called whenever an element identifier is about + // to be written to the stream. |element_id| is the element identifier, and + // |position| is the location in the WebM stream where the first octet of the + // element identifier will be written. + // Note: the |MkvId| enumeration in webmids.hpp defines element values. + virtual void ElementStartNotify(uint64 element_id, int64 position) = 0; + + protected: + IMkvWriter(); + virtual ~IMkvWriter(); + + private: + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(IMkvWriter); +}; + +// Writes out the EBML header for a WebM file, but allows caller to specify +// DocType. This function must be called before any other libwebm writing +// functions are called. +bool WriteEbmlHeader(IMkvWriter* writer, uint64_t doc_type_version, + const char* const doc_type); + +// Writes out the EBML header for a WebM file. This function must be called +// before any other libwebm writing functions are called. +bool WriteEbmlHeader(IMkvWriter* writer, uint64_t doc_type_version); + +// Deprecated. Writes out EBML header with doc_type_version as +// kDefaultDocTypeVersion. Exists for backward compatibility. +bool WriteEbmlHeader(IMkvWriter* writer); + +// Copies in Chunk from source to destination between the given byte positions +bool ChunkedCopy(mkvparser::IMkvReader* source, IMkvWriter* dst, int64_t start, + int64_t size); + +/////////////////////////////////////////////////////////////// +// Class to hold data the will be written to a block. +class Frame { + public: + Frame(); + ~Frame(); + + // Sets this frame's contents based on |frame|. Returns true on success. On + // failure, this frame's existing contents may be lost. + bool CopyFrom(const Frame& frame); + + // Copies |frame| data into |frame_|. Returns true on success. + bool Init(const uint8_t* frame, uint64_t length); + + // Copies |additional| data into |additional_|. Returns true on success. + bool AddAdditionalData(const uint8_t* additional, uint64_t length, + uint64_t add_id); + + // Returns true if the frame has valid parameters. + bool IsValid() const; + + // Returns true if the frame can be written as a SimpleBlock based on current + // parameters. + bool CanBeSimpleBlock() const; + + uint64_t add_id() const { return add_id_; } + const uint8_t* additional() const { return additional_; } + uint64_t additional_length() const { return additional_length_; } + void set_duration(uint64_t duration); + uint64_t duration() const { return duration_; } + bool duration_set() const { return duration_set_; } + const uint8_t* frame() const { return frame_; } + void set_is_key(bool key) { is_key_ = key; } + bool is_key() const { return is_key_; } + uint64_t length() const { return length_; } + void set_track_number(uint64_t track_number) { track_number_ = track_number; } + uint64_t track_number() const { return track_number_; } + void set_timestamp(uint64_t timestamp) { timestamp_ = timestamp; } + uint64_t timestamp() const { return timestamp_; } + void set_discard_padding(int64_t discard_padding) { + discard_padding_ = discard_padding; + } + int64_t discard_padding() const { return discard_padding_; } + void set_reference_block_timestamp(int64_t reference_block_timestamp); + int64_t reference_block_timestamp() const { + return reference_block_timestamp_; + } + bool reference_block_timestamp_set() const { + return reference_block_timestamp_set_; + } + + private: + // Id of the Additional data. + uint64_t add_id_; + + // Pointer to additional data. Owned by this class. + uint8_t* additional_; + + // Length of the additional data. + uint64_t additional_length_; + + // Duration of the frame in nanoseconds. + uint64_t duration_; + + // Flag indicating that |duration_| has been set. Setting duration causes the + // frame to be written out as a Block with BlockDuration instead of as a + // SimpleBlock. + bool duration_set_; + + // Pointer to the data. Owned by this class. + uint8_t* frame_; + + // Flag telling if the data should set the key flag of a block. + bool is_key_; + + // Length of the data. + uint64_t length_; + + // Mkv track number the data is associated with. + uint64_t track_number_; + + // Timestamp of the data in nanoseconds. + uint64_t timestamp_; + + // Discard padding for the frame. + int64_t discard_padding_; + + // Reference block timestamp. + int64_t reference_block_timestamp_; + + // Flag indicating if |reference_block_timestamp_| has been set. + bool reference_block_timestamp_set_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Frame); +}; + +/////////////////////////////////////////////////////////////// +// Class to hold one cue point in a Cues element. +class CuePoint { + public: + CuePoint(); + ~CuePoint(); + + // Returns the size in bytes for the entire CuePoint element. + uint64_t Size() const; + + // Output the CuePoint element to the writer. Returns true on success. + bool Write(IMkvWriter* writer) const; + + void set_time(uint64_t time) { time_ = time; } + uint64_t time() const { return time_; } + void set_track(uint64_t track) { track_ = track; } + uint64_t track() const { return track_; } + void set_cluster_pos(uint64_t cluster_pos) { cluster_pos_ = cluster_pos; } + uint64_t cluster_pos() const { return cluster_pos_; } + void set_block_number(uint64_t block_number) { block_number_ = block_number; } + uint64_t block_number() const { return block_number_; } + void set_output_block_number(bool output_block_number) { + output_block_number_ = output_block_number; + } + bool output_block_number() const { return output_block_number_; } + + private: + // Returns the size in bytes for the payload of the CuePoint element. + uint64_t PayloadSize() const; + + // Absolute timecode according to the segment time base. + uint64_t time_; + + // The Track element associated with the CuePoint. + uint64_t track_; + + // The position of the Cluster containing the Block. + uint64_t cluster_pos_; + + // Number of the Block within the Cluster, starting from 1. + uint64_t block_number_; + + // If true the muxer will write out the block number for the cue if the + // block number is different than the default of 1. Default is set to true. + bool output_block_number_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(CuePoint); +}; + +/////////////////////////////////////////////////////////////// +// Cues element. +class Cues { + public: + Cues(); + ~Cues(); + + // Adds a cue point to the Cues element. Returns true on success. + bool AddCue(CuePoint* cue); + + // Returns the cue point by index. Returns NULL if there is no cue point + // match. + CuePoint* GetCueByIndex(int32_t index) const; + + // Returns the total size of the Cues element + uint64_t Size(); + + // Output the Cues element to the writer. Returns true on success. + bool Write(IMkvWriter* writer) const; + + int32_t cue_entries_size() const { return cue_entries_size_; } + void set_output_block_number(bool output_block_number) { + output_block_number_ = output_block_number; + } + bool output_block_number() const { return output_block_number_; } + + private: + // Number of allocated elements in |cue_entries_|. + int32_t cue_entries_capacity_; + + // Number of CuePoints in |cue_entries_|. + int32_t cue_entries_size_; + + // CuePoint list. + CuePoint** cue_entries_; + + // If true the muxer will write out the block number for the cue if the + // block number is different than the default of 1. Default is set to true. + bool output_block_number_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Cues); +}; + +/////////////////////////////////////////////////////////////// +// ContentEncAESSettings element +class ContentEncAESSettings { + public: + enum { kCTR = 1 }; + + ContentEncAESSettings(); + ~ContentEncAESSettings() {} + + // Returns the size in bytes for the ContentEncAESSettings element. + uint64_t Size() const; + + // Writes out the ContentEncAESSettings element to |writer|. Returns true on + // success. + bool Write(IMkvWriter* writer) const; + + uint64_t cipher_mode() const { return cipher_mode_; } + + private: + // Returns the size in bytes for the payload of the ContentEncAESSettings + // element. + uint64_t PayloadSize() const; + + // Sub elements + uint64_t cipher_mode_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(ContentEncAESSettings); +}; + +/////////////////////////////////////////////////////////////// +// ContentEncoding element +// Elements used to describe if the track data has been encrypted or +// compressed with zlib or header stripping. +// Currently only whole frames can be encrypted with AES. This dictates that +// ContentEncodingOrder will be 0, ContentEncodingScope will be 1, +// ContentEncodingType will be 1, and ContentEncAlgo will be 5. +class ContentEncoding { + public: + ContentEncoding(); + ~ContentEncoding(); + + // Sets the content encryption id. Copies |length| bytes from |id| to + // |enc_key_id_|. Returns true on success. + bool SetEncryptionID(const uint8_t* id, uint64_t length); + + // Returns the size in bytes for the ContentEncoding element. + uint64_t Size() const; + + // Writes out the ContentEncoding element to |writer|. Returns true on + // success. + bool Write(IMkvWriter* writer) const; + + uint64_t enc_algo() const { return enc_algo_; } + uint64_t encoding_order() const { return encoding_order_; } + uint64_t encoding_scope() const { return encoding_scope_; } + uint64_t encoding_type() const { return encoding_type_; } + ContentEncAESSettings* enc_aes_settings() { return &enc_aes_settings_; } + + private: + // Returns the size in bytes for the encoding elements. + uint64_t EncodingSize(uint64_t compresion_size, + uint64_t encryption_size) const; + + // Returns the size in bytes for the encryption elements. + uint64_t EncryptionSize() const; + + // Track element names + uint64_t enc_algo_; + uint8_t* enc_key_id_; + uint64_t encoding_order_; + uint64_t encoding_scope_; + uint64_t encoding_type_; + + // ContentEncAESSettings element. + ContentEncAESSettings enc_aes_settings_; + + // Size of the ContentEncKeyID data in bytes. + uint64_t enc_key_id_length_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(ContentEncoding); +}; + +/////////////////////////////////////////////////////////////// +// Colour element. +struct PrimaryChromaticity { + PrimaryChromaticity(float x_val, float y_val) : x(x_val), y(y_val) {} + PrimaryChromaticity() : x(0), y(0) {} + ~PrimaryChromaticity() {} + uint64_t PrimaryChromaticityPayloadSize(libwebm::MkvId x_id, + libwebm::MkvId y_id) const; + bool Write(IMkvWriter* writer, libwebm::MkvId x_id, + libwebm::MkvId y_id) const; + + float x; + float y; +}; + +class MasteringMetadata { + public: + static const float kValueNotPresent; + + MasteringMetadata() + : luminance_max(kValueNotPresent), + luminance_min(kValueNotPresent), + r_(NULL), + g_(NULL), + b_(NULL), + white_point_(NULL) {} + ~MasteringMetadata() { + delete r_; + delete g_; + delete b_; + delete white_point_; + } + + // Returns total size of the MasteringMetadata element. + uint64_t MasteringMetadataSize() const; + bool Write(IMkvWriter* writer) const; + + // Copies non-null chromaticity. + bool SetChromaticity(const PrimaryChromaticity* r, + const PrimaryChromaticity* g, + const PrimaryChromaticity* b, + const PrimaryChromaticity* white_point); + const PrimaryChromaticity* r() const { return r_; } + const PrimaryChromaticity* g() const { return g_; } + const PrimaryChromaticity* b() const { return b_; } + const PrimaryChromaticity* white_point() const { return white_point_; } + + float luminance_max; + float luminance_min; + + private: + // Returns size of MasteringMetadata child elements. + uint64_t PayloadSize() const; + + PrimaryChromaticity* r_; + PrimaryChromaticity* g_; + PrimaryChromaticity* b_; + PrimaryChromaticity* white_point_; +}; + +class Colour { + public: + static const uint64_t kValueNotPresent; + Colour() + : matrix_coefficients(kValueNotPresent), + bits_per_channel(kValueNotPresent), + chroma_subsampling_horz(kValueNotPresent), + chroma_subsampling_vert(kValueNotPresent), + cb_subsampling_horz(kValueNotPresent), + cb_subsampling_vert(kValueNotPresent), + chroma_siting_horz(kValueNotPresent), + chroma_siting_vert(kValueNotPresent), + range(kValueNotPresent), + transfer_characteristics(kValueNotPresent), + primaries(kValueNotPresent), + max_cll(kValueNotPresent), + max_fall(kValueNotPresent), + mastering_metadata_(NULL) {} + ~Colour() { delete mastering_metadata_; } + + // Returns total size of the Colour element. + uint64_t ColourSize() const; + bool Write(IMkvWriter* writer) const; + + // Deep copies |mastering_metadata|. + bool SetMasteringMetadata(const MasteringMetadata& mastering_metadata); + + const MasteringMetadata* mastering_metadata() const { + return mastering_metadata_; + } + + uint64_t matrix_coefficients; + uint64_t bits_per_channel; + uint64_t chroma_subsampling_horz; + uint64_t chroma_subsampling_vert; + uint64_t cb_subsampling_horz; + uint64_t cb_subsampling_vert; + uint64_t chroma_siting_horz; + uint64_t chroma_siting_vert; + uint64_t range; + uint64_t transfer_characteristics; + uint64_t primaries; + uint64_t max_cll; + uint64_t max_fall; + + private: + // Returns size of Colour child elements. + uint64_t PayloadSize() const; + + MasteringMetadata* mastering_metadata_; +}; + +/////////////////////////////////////////////////////////////// +// Track element. +class Track { + public: + // The |seed| parameter is used to synthesize a UID for the track. + explicit Track(unsigned int* seed); + virtual ~Track(); + + // Adds a ContentEncoding element to the Track. Returns true on success. + virtual bool AddContentEncoding(); + + // Returns the ContentEncoding by index. Returns NULL if there is no + // ContentEncoding match. + ContentEncoding* GetContentEncodingByIndex(uint32_t index) const; + + // Returns the size in bytes for the payload of the Track element. + virtual uint64_t PayloadSize() const; + + // Returns the size in bytes of the Track element. + virtual uint64_t Size() const; + + // Output the Track element to the writer. Returns true on success. + virtual bool Write(IMkvWriter* writer) const; + + // Sets the CodecPrivate element of the Track element. Copies |length| + // bytes from |codec_private| to |codec_private_|. Returns true on success. + bool SetCodecPrivate(const uint8_t* codec_private, uint64_t length); + + void set_codec_id(const char* codec_id); + const char* codec_id() const { return codec_id_; } + const uint8_t* codec_private() const { return codec_private_; } + void set_language(const char* language); + const char* language() const { return language_; } + void set_max_block_additional_id(uint64_t max_block_additional_id) { + max_block_additional_id_ = max_block_additional_id; + } + uint64_t max_block_additional_id() const { return max_block_additional_id_; } + void set_name(const char* name); + const char* name() const { return name_; } + void set_number(uint64_t number) { number_ = number; } + uint64_t number() const { return number_; } + void set_type(uint64_t type) { type_ = type; } + uint64_t type() const { return type_; } + void set_uid(uint64_t uid) { uid_ = uid; } + uint64_t uid() const { return uid_; } + void set_codec_delay(uint64_t codec_delay) { codec_delay_ = codec_delay; } + uint64_t codec_delay() const { return codec_delay_; } + void set_seek_pre_roll(uint64_t seek_pre_roll) { + seek_pre_roll_ = seek_pre_roll; + } + uint64_t seek_pre_roll() const { return seek_pre_roll_; } + void set_default_duration(uint64_t default_duration) { + default_duration_ = default_duration; + } + uint64_t default_duration() const { return default_duration_; } + + uint64_t codec_private_length() const { return codec_private_length_; } + uint32_t content_encoding_entries_size() const { + return content_encoding_entries_size_; + } + + private: + // Track element names. + char* codec_id_; + uint8_t* codec_private_; + char* language_; + uint64_t max_block_additional_id_; + char* name_; + uint64_t number_; + uint64_t type_; + uint64_t uid_; + uint64_t codec_delay_; + uint64_t seek_pre_roll_; + uint64_t default_duration_; + + // Size of the CodecPrivate data in bytes. + uint64_t codec_private_length_; + + // ContentEncoding element list. + ContentEncoding** content_encoding_entries_; + + // Number of ContentEncoding elements added. + uint32_t content_encoding_entries_size_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Track); +}; + +/////////////////////////////////////////////////////////////// +// Track that has video specific elements. +class VideoTrack : public Track { + public: + // Supported modes for stereo 3D. + enum StereoMode { + kMono = 0, + kSideBySideLeftIsFirst = 1, + kTopBottomRightIsFirst = 2, + kTopBottomLeftIsFirst = 3, + kSideBySideRightIsFirst = 11 + }; + + enum AlphaMode { kNoAlpha = 0, kAlpha = 1 }; + + // The |seed| parameter is used to synthesize a UID for the track. + explicit VideoTrack(unsigned int* seed); + virtual ~VideoTrack(); + + // Returns the size in bytes for the payload of the Track element plus the + // video specific elements. + virtual uint64_t PayloadSize() const; + + // Output the VideoTrack element to the writer. Returns true on success. + virtual bool Write(IMkvWriter* writer) const; + + // Sets the video's stereo mode. Returns true on success. + bool SetStereoMode(uint64_t stereo_mode); + + // Sets the video's alpha mode. Returns true on success. + bool SetAlphaMode(uint64_t alpha_mode); + + void set_display_height(uint64_t height) { display_height_ = height; } + uint64_t display_height() const { return display_height_; } + void set_display_width(uint64_t width) { display_width_ = width; } + uint64_t display_width() const { return display_width_; } + + void set_crop_left(uint64_t crop_left) { crop_left_ = crop_left; } + uint64_t crop_left() const { return crop_left_; } + void set_crop_right(uint64_t crop_right) { crop_right_ = crop_right; } + uint64_t crop_right() const { return crop_right_; } + void set_crop_top(uint64_t crop_top) { crop_top_ = crop_top; } + uint64_t crop_top() const { return crop_top_; } + void set_crop_bottom(uint64_t crop_bottom) { crop_bottom_ = crop_bottom; } + uint64_t crop_bottom() const { return crop_bottom_; } + + void set_frame_rate(double frame_rate) { frame_rate_ = frame_rate; } + double frame_rate() const { return frame_rate_; } + void set_height(uint64_t height) { height_ = height; } + uint64_t height() const { return height_; } + uint64_t stereo_mode() { return stereo_mode_; } + uint64_t alpha_mode() { return alpha_mode_; } + void set_width(uint64_t width) { width_ = width; } + uint64_t width() const { return width_; } + + Colour* colour() { return colour_; } + + // Deep copies |colour|. + bool SetColour(const Colour& colour); + + private: + // Returns the size in bytes of the Video element. + uint64_t VideoPayloadSize() const; + + // Video track element names. + uint64_t display_height_; + uint64_t display_width_; + uint64_t crop_left_; + uint64_t crop_right_; + uint64_t crop_top_; + uint64_t crop_bottom_; + double frame_rate_; + uint64_t height_; + uint64_t stereo_mode_; + uint64_t alpha_mode_; + uint64_t width_; + + Colour* colour_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(VideoTrack); +}; + +/////////////////////////////////////////////////////////////// +// Track that has audio specific elements. +class AudioTrack : public Track { + public: + // The |seed| parameter is used to synthesize a UID for the track. + explicit AudioTrack(unsigned int* seed); + virtual ~AudioTrack(); + + // Returns the size in bytes for the payload of the Track element plus the + // audio specific elements. + virtual uint64_t PayloadSize() const; + + // Output the AudioTrack element to the writer. Returns true on success. + virtual bool Write(IMkvWriter* writer) const; + + void set_bit_depth(uint64_t bit_depth) { bit_depth_ = bit_depth; } + uint64_t bit_depth() const { return bit_depth_; } + void set_channels(uint64_t channels) { channels_ = channels; } + uint64_t channels() const { return channels_; } + void set_sample_rate(double sample_rate) { sample_rate_ = sample_rate; } + double sample_rate() const { return sample_rate_; } + + private: + // Audio track element names. + uint64_t bit_depth_; + uint64_t channels_; + double sample_rate_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(AudioTrack); +}; + +/////////////////////////////////////////////////////////////// +// Tracks element +class Tracks { + public: + // Audio and video type defined by the Matroska specs. + enum { kVideo = 0x1, kAudio = 0x2 }; + + static const char kOpusCodecId[]; + static const char kVorbisCodecId[]; + static const char kVp8CodecId[]; + static const char kVp9CodecId[]; + static const char kVp10CodecId[]; + static const char kWebVttCaptionsId[]; + static const char kWebVttDescriptionsId[]; + static const char kWebVttMetadataId[]; + static const char kWebVttSubtitlesId[]; + + Tracks(); + ~Tracks(); + + // Adds a Track element to the Tracks object. |track| will be owned and + // deleted by the Tracks object. Returns true on success. |number| is the + // number to use for the track. |number| must be >= 0. If |number| == 0 + // then the muxer will decide on the track number. + bool AddTrack(Track* track, int32_t number); + + // Returns the track by index. Returns NULL if there is no track match. + const Track* GetTrackByIndex(uint32_t idx) const; + + // Search the Tracks and return the track that matches |tn|. Returns NULL + // if there is no track match. + Track* GetTrackByNumber(uint64_t track_number) const; + + // Returns true if the track number is an audio track. + bool TrackIsAudio(uint64_t track_number) const; + + // Returns true if the track number is a video track. + bool TrackIsVideo(uint64_t track_number) const; + + // Output the Tracks element to the writer. Returns true on success. + bool Write(IMkvWriter* writer) const; + + uint32_t track_entries_size() const { return track_entries_size_; } + + private: + // Track element list. + Track** track_entries_; + + // Number of Track elements added. + uint32_t track_entries_size_; + + // Whether or not Tracks element has already been written via IMkvWriter. + mutable bool wrote_tracks_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Tracks); +}; + +/////////////////////////////////////////////////////////////// +// Chapter element +// +class Chapter { + public: + // Set the identifier for this chapter. (This corresponds to the + // Cue Identifier line in WebVTT.) + // TODO(matthewjheaney): the actual serialization of this item in + // MKV is pending. + bool set_id(const char* id); + + // Converts the nanosecond start and stop times of this chapter to + // their corresponding timecode values, and stores them that way. + void set_time(const Segment& segment, uint64_t start_time_ns, + uint64_t end_time_ns); + + // Sets the uid for this chapter. Primarily used to enable + // deterministic output from the muxer. + void set_uid(const uint64_t uid) { uid_ = uid; } + + // Add a title string to this chapter, per the semantics described + // here: + // http://www.matroska.org/technical/specs/index.html + // + // The title ("chapter string") is a UTF-8 string. + // + // The language has ISO 639-2 representation, described here: + // http://www.loc.gov/standards/iso639-2/englangn.html + // http://www.loc.gov/standards/iso639-2/php/English_list.php + // If you specify NULL as the language value, this implies + // English ("eng"). + // + // The country value corresponds to the codes listed here: + // http://www.iana.org/domains/root/db/ + // + // The function returns false if the string could not be allocated. + bool add_string(const char* title, const char* language, const char* country); + + private: + friend class Chapters; + + // For storage of chapter titles that differ by language. + class Display { + public: + // Establish representation invariant for new Display object. + void Init(); + + // Reclaim resources, in anticipation of destruction. + void Clear(); + + // Copies the title to the |title_| member. Returns false on + // error. + bool set_title(const char* title); + + // Copies the language to the |language_| member. Returns false + // on error. + bool set_language(const char* language); + + // Copies the country to the |country_| member. Returns false on + // error. + bool set_country(const char* country); + + // If |writer| is non-NULL, serialize the Display sub-element of + // the Atom into the stream. Returns the Display element size on + // success, 0 if error. + uint64_t WriteDisplay(IMkvWriter* writer) const; + + private: + char* title_; + char* language_; + char* country_; + }; + + Chapter(); + ~Chapter(); + + // Establish the representation invariant for a newly-created + // Chapter object. The |seed| parameter is used to create the UID + // for this chapter atom. + void Init(unsigned int* seed); + + // Copies this Chapter object to a different one. This is used when + // expanding a plain array of Chapter objects (see Chapters). + void ShallowCopy(Chapter* dst) const; + + // Reclaim resources used by this Chapter object, pending its + // destruction. + void Clear(); + + // If there is no storage remaining on the |displays_| array for a + // new display object, creates a new, longer array and copies the + // existing Display objects to the new array. Returns false if the + // array cannot be expanded. + bool ExpandDisplaysArray(); + + // If |writer| is non-NULL, serialize the Atom sub-element into the + // stream. Returns the total size of the element on success, 0 if + // error. + uint64_t WriteAtom(IMkvWriter* writer) const; + + // The string identifier for this chapter (corresponds to WebVTT cue + // identifier). + char* id_; + + // Start timecode of the chapter. + uint64_t start_timecode_; + + // Stop timecode of the chapter. + uint64_t end_timecode_; + + // The binary identifier for this chapter. + uint64_t uid_; + + // The Atom element can contain multiple Display sub-elements, as + // the same logical title can be rendered in different languages. + Display* displays_; + + // The physical length (total size) of the |displays_| array. + int displays_size_; + + // The logical length (number of active elements) on the |displays_| + // array. + int displays_count_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Chapter); +}; + +/////////////////////////////////////////////////////////////// +// Chapters element +// +class Chapters { + public: + Chapters(); + ~Chapters(); + + Chapter* AddChapter(unsigned int* seed); + + // Returns the number of chapters that have been added. + int Count() const; + + // Output the Chapters element to the writer. Returns true on success. + bool Write(IMkvWriter* writer) const; + + private: + // Expands the chapters_ array if there is not enough space to contain + // another chapter object. Returns true on success. + bool ExpandChaptersArray(); + + // If |writer| is non-NULL, serialize the Edition sub-element of the + // Chapters element into the stream. Returns the Edition element + // size on success, 0 if error. + uint64_t WriteEdition(IMkvWriter* writer) const; + + // Total length of the chapters_ array. + int chapters_size_; + + // Number of active chapters on the chapters_ array. + int chapters_count_; + + // Array for storage of chapter objects. + Chapter* chapters_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Chapters); +}; + +/////////////////////////////////////////////////////////////// +// Tag element +// +class Tag { + public: + bool add_simple_tag(const char* tag_name, const char* tag_string); + + private: + // Tags calls Clear and the destructor of Tag + friend class Tags; + + // For storage of simple tags + class SimpleTag { + public: + // Establish representation invariant for new SimpleTag object. + void Init(); + + // Reclaim resources, in anticipation of destruction. + void Clear(); + + // Copies the title to the |tag_name_| member. Returns false on + // error. + bool set_tag_name(const char* tag_name); + + // Copies the language to the |tag_string_| member. Returns false + // on error. + bool set_tag_string(const char* tag_string); + + // If |writer| is non-NULL, serialize the SimpleTag sub-element of + // the Atom into the stream. Returns the SimpleTag element size on + // success, 0 if error. + uint64_t Write(IMkvWriter* writer) const; + + private: + char* tag_name_; + char* tag_string_; + }; + + Tag(); + ~Tag(); + + // Copies this Tag object to a different one. This is used when + // expanding a plain array of Tag objects (see Tags). + void ShallowCopy(Tag* dst) const; + + // Reclaim resources used by this Tag object, pending its + // destruction. + void Clear(); + + // If there is no storage remaining on the |simple_tags_| array for a + // new display object, creates a new, longer array and copies the + // existing SimpleTag objects to the new array. Returns false if the + // array cannot be expanded. + bool ExpandSimpleTagsArray(); + + // If |writer| is non-NULL, serialize the Tag sub-element into the + // stream. Returns the total size of the element on success, 0 if + // error. + uint64_t Write(IMkvWriter* writer) const; + + // The Atom element can contain multiple SimpleTag sub-elements + SimpleTag* simple_tags_; + + // The physical length (total size) of the |simple_tags_| array. + int simple_tags_size_; + + // The logical length (number of active elements) on the |simple_tags_| + // array. + int simple_tags_count_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Tag); +}; + +/////////////////////////////////////////////////////////////// +// Tags element +// +class Tags { + public: + Tags(); + ~Tags(); + + Tag* AddTag(); + + // Returns the number of tags that have been added. + int Count() const; + + // Output the Tags element to the writer. Returns true on success. + bool Write(IMkvWriter* writer) const; + + private: + // Expands the tags_ array if there is not enough space to contain + // another tag object. Returns true on success. + bool ExpandTagsArray(); + + // Total length of the tags_ array. + int tags_size_; + + // Number of active tags on the tags_ array. + int tags_count_; + + // Array for storage of tag objects. + Tag* tags_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Tags); +}; + +/////////////////////////////////////////////////////////////// +// Cluster element +// +// Notes: +// |Init| must be called before any other method in this class. +class Cluster { + public: + // |timecode| is the absolute timecode of the cluster. |cues_pos| is the + // position for the cluster within the segment that should be written in + // the cues element. |timecode_scale| is the timecode scale of the segment. + Cluster(uint64_t timecode, int64_t cues_pos, uint64_t timecode_scale, + bool write_last_frame_with_duration = false, + bool fixed_size_timecode = false); + ~Cluster(); + + bool Init(IMkvWriter* ptr_writer); + + // Adds a frame to be output in the file. The frame is written out through + // |writer_| if successful. Returns true on success. + bool AddFrame(const Frame* frame); + + // Adds a frame to be output in the file. The frame is written out through + // |writer_| if successful. Returns true on success. + // Inputs: + // data: Pointer to the data + // length: Length of the data + // track_number: Track to add the data to. Value returned by Add track + // functions. The range of allowed values is [1, 126]. + // timecode: Absolute (not relative to cluster) timestamp of the + // frame, expressed in timecode units. + // is_key: Flag telling whether or not this frame is a key frame. + bool AddFrame(const uint8_t* data, uint64_t length, uint64_t track_number, + uint64_t timecode, // timecode units (absolute) + bool is_key); + + // Adds a frame to be output in the file. The frame is written out through + // |writer_| if successful. Returns true on success. + // Inputs: + // data: Pointer to the data + // length: Length of the data + // additional: Pointer to the additional data + // additional_length: Length of the additional data + // add_id: Value of BlockAddID element + // track_number: Track to add the data to. Value returned by Add track + // functions. The range of allowed values is [1, 126]. + // abs_timecode: Absolute (not relative to cluster) timestamp of the + // frame, expressed in timecode units. + // is_key: Flag telling whether or not this frame is a key frame. + bool AddFrameWithAdditional(const uint8_t* data, uint64_t length, + const uint8_t* additional, + uint64_t additional_length, uint64_t add_id, + uint64_t track_number, uint64_t abs_timecode, + bool is_key); + + // Adds a frame to be output in the file. The frame is written out through + // |writer_| if successful. Returns true on success. + // Inputs: + // data: Pointer to the data. + // length: Length of the data. + // discard_padding: DiscardPadding element value. + // track_number: Track to add the data to. Value returned by Add track + // functions. The range of allowed values is [1, 126]. + // abs_timecode: Absolute (not relative to cluster) timestamp of the + // frame, expressed in timecode units. + // is_key: Flag telling whether or not this frame is a key frame. + bool AddFrameWithDiscardPadding(const uint8_t* data, uint64_t length, + int64_t discard_padding, + uint64_t track_number, uint64_t abs_timecode, + bool is_key); + + // Writes a frame of metadata to the output medium; returns true on + // success. + // Inputs: + // data: Pointer to the data + // length: Length of the data + // track_number: Track to add the data to. Value returned by Add track + // functions. The range of allowed values is [1, 126]. + // timecode: Absolute (not relative to cluster) timestamp of the + // metadata frame, expressed in timecode units. + // duration: Duration of metadata frame, in timecode units. + // + // The metadata frame is written as a block group, with a duration + // sub-element but no reference time sub-elements (indicating that + // it is considered a keyframe, per Matroska semantics). + bool AddMetadata(const uint8_t* data, uint64_t length, uint64_t track_number, + uint64_t timecode, uint64_t duration); + + // Increments the size of the cluster's data in bytes. + void AddPayloadSize(uint64_t size); + + // Closes the cluster so no more data can be written to it. Will update the + // cluster's size if |writer_| is seekable. Returns true on success. This + // variant of Finalize() fails when |write_last_frame_with_duration_| is set + // to true. + bool Finalize(); + + // Closes the cluster so no more data can be written to it. Will update the + // cluster's size if |writer_| is seekable. Returns true on success. + // Inputs: + // set_last_frame_duration: Boolean indicating whether or not the duration + // of the last frame should be set. If set to + // false, the |duration| value is ignored and + // |write_last_frame_with_duration_| will not be + // honored. + // duration: Duration of the Cluster in timecode scale. + bool Finalize(bool set_last_frame_duration, uint64_t duration); + + // Returns the size in bytes for the entire Cluster element. + uint64_t Size() const; + + // Given |abs_timecode|, calculates timecode relative to most recent timecode. + // Returns -1 on failure, or a relative timecode. + int64_t GetRelativeTimecode(int64_t abs_timecode) const; + + int64_t size_position() const { return size_position_; } + int32_t blocks_added() const { return blocks_added_; } + uint64_t payload_size() const { return payload_size_; } + int64_t position_for_cues() const { return position_for_cues_; } + uint64_t timecode() const { return timecode_; } + uint64_t timecode_scale() const { return timecode_scale_; } + void set_write_last_frame_with_duration(bool write_last_frame_with_duration) { + write_last_frame_with_duration_ = write_last_frame_with_duration; + } + bool write_last_frame_with_duration() const { + return write_last_frame_with_duration_; + } + + private: + // Iterator type for the |stored_frames_| map. + typedef std::map >::iterator FrameMapIterator; + + // Utility method that confirms that blocks can still be added, and that the + // cluster header has been written. Used by |DoWriteFrame*|. Returns true + // when successful. + bool PreWriteBlock(); + + // Utility method used by the |DoWriteFrame*| methods that handles the book + // keeping required after each block is written. + void PostWriteBlock(uint64_t element_size); + + // Does some verification and calls WriteFrame. + bool DoWriteFrame(const Frame* const frame); + + // Either holds back the given frame, or writes it out depending on whether or + // not |write_last_frame_with_duration_| is set. + bool QueueOrWriteFrame(const Frame* const frame); + + // Outputs the Cluster header to |writer_|. Returns true on success. + bool WriteClusterHeader(); + + // Number of blocks added to the cluster. + int32_t blocks_added_; + + // Flag telling if the cluster has been closed. + bool finalized_; + + // Flag indicating whether the cluster's timecode will always be written out + // using 8 bytes. + bool fixed_size_timecode_; + + // Flag telling if the cluster's header has been written. + bool header_written_; + + // The size of the cluster elements in bytes. + uint64_t payload_size_; + + // The file position used for cue points. + const int64_t position_for_cues_; + + // The file position of the cluster's size element. + int64_t size_position_; + + // The absolute timecode of the cluster. + const uint64_t timecode_; + + // The timecode scale of the Segment containing the cluster. + const uint64_t timecode_scale_; + + // Flag indicating whether the last frame of the cluster should be written as + // a Block with Duration. If set to true, then it will result in holding back + // of frames and the parameterized version of Finalize() must be called to + // finish writing the Cluster. + bool write_last_frame_with_duration_; + + // Map used to hold back frames, if required. Track number is the key. + std::map > stored_frames_; + + // Map from track number to the timestamp of the last block written for that + // track. + std::map last_block_timestamp_; + + // Pointer to the writer object. Not owned by this class. + IMkvWriter* writer_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Cluster); +}; + +/////////////////////////////////////////////////////////////// +// SeekHead element +class SeekHead { + public: + SeekHead(); + ~SeekHead(); + + // TODO(fgalligan): Change this to reserve a certain size. Then check how + // big the seek entry to be added is as not every seek entry will be the + // maximum size it could be. + // Adds a seek entry to be written out when the element is finalized. |id| + // must be the coded mkv element id. |pos| is the file position of the + // element. Returns true on success. + bool AddSeekEntry(uint32_t id, uint64_t pos); + + // Writes out SeekHead and SeekEntry elements. Returns true on success. + bool Finalize(IMkvWriter* writer) const; + + // Returns the id of the Seek Entry at the given index. Returns -1 if index is + // out of range. + uint32_t GetId(int index) const; + + // Returns the position of the Seek Entry at the given index. Returns -1 if + // index is out of range. + uint64_t GetPosition(int index) const; + + // Sets the Seek Entry id and position at given index. + // Returns true on success. + bool SetSeekEntry(int index, uint32_t id, uint64_t position); + + // Reserves space by writing out a Void element which will be updated with + // a SeekHead element later. Returns true on success. + bool Write(IMkvWriter* writer); + + // We are going to put a cap on the number of Seek Entries. + const static int32_t kSeekEntryCount = 5; + + private: + // Returns the maximum size in bytes of one seek entry. + uint64_t MaxEntrySize() const; + + // Seek entry id element list. + uint32_t seek_entry_id_[kSeekEntryCount]; + + // Seek entry pos element list. + uint64_t seek_entry_pos_[kSeekEntryCount]; + + // The file position of SeekHead element. + int64_t start_pos_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(SeekHead); +}; + +/////////////////////////////////////////////////////////////// +// Segment Information element +class SegmentInfo { + public: + SegmentInfo(); + ~SegmentInfo(); + + // Will update the duration if |duration_| is > 0.0. Returns true on success. + bool Finalize(IMkvWriter* writer) const; + + // Sets |muxing_app_| and |writing_app_|. + bool Init(); + + // Output the Segment Information element to the writer. Returns true on + // success. + bool Write(IMkvWriter* writer); + + void set_duration(double duration) { duration_ = duration; } + double duration() const { return duration_; } + void set_muxing_app(const char* app); + const char* muxing_app() const { return muxing_app_; } + void set_timecode_scale(uint64_t scale) { timecode_scale_ = scale; } + uint64_t timecode_scale() const { return timecode_scale_; } + void set_writing_app(const char* app); + const char* writing_app() const { return writing_app_; } + void set_date_utc(int64_t date_utc) { date_utc_ = date_utc; } + int64_t date_utc() const { return date_utc_; } + + private: + // Segment Information element names. + // Initially set to -1 to signify that a duration has not been set and should + // not be written out. + double duration_; + // Set to libwebm-%d.%d.%d.%d, major, minor, build, revision. + char* muxing_app_; + uint64_t timecode_scale_; + // Initially set to libwebm-%d.%d.%d.%d, major, minor, build, revision. + char* writing_app_; + // LLONG_MIN when DateUTC is not set. + int64_t date_utc_; + + // The file position of the duration element. + int64_t duration_pos_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(SegmentInfo); +}; + +/////////////////////////////////////////////////////////////// +// This class represents the main segment in a WebM file. Currently only +// supports one Segment element. +// +// Notes: +// |Init| must be called before any other method in this class. +class Segment { + public: + enum Mode { kLive = 0x1, kFile = 0x2 }; + + enum CuesPosition { + kAfterClusters = 0x0, // Position Cues after Clusters - Default + kBeforeClusters = 0x1 // Position Cues before Clusters + }; + + const static uint32_t kDefaultDocTypeVersion = 2; + const static uint64_t kDefaultMaxClusterDuration = 30000000000ULL; + + Segment(); + ~Segment(); + + // Initializes |SegmentInfo| and returns result. Always returns false when + // |ptr_writer| is NULL. + bool Init(IMkvWriter* ptr_writer); + + // Adds a generic track to the segment. Returns the newly-allocated + // track object (which is owned by the segment) on success, NULL on + // error. |number| is the number to use for the track. |number| + // must be >= 0. If |number| == 0 then the muxer will decide on the + // track number. + Track* AddTrack(int32_t number); + + // Adds a Vorbis audio track to the segment. Returns the number of the track + // on success, 0 on error. |number| is the number to use for the audio track. + // |number| must be >= 0. If |number| == 0 then the muxer will decide on + // the track number. + uint64_t AddAudioTrack(int32_t sample_rate, int32_t channels, int32_t number); + + // Adds an empty chapter to the chapters of this segment. Returns + // non-NULL on success. After adding the chapter, the caller should + // populate its fields via the Chapter member functions. + Chapter* AddChapter(); + + // Adds an empty tag to the tags of this segment. Returns + // non-NULL on success. After adding the tag, the caller should + // populate its fields via the Tag member functions. + Tag* AddTag(); + + // Adds a cue point to the Cues element. |timestamp| is the time in + // nanoseconds of the cue's time. |track| is the Track of the Cue. This + // function must be called after AddFrame to calculate the correct + // BlockNumber for the CuePoint. Returns true on success. + bool AddCuePoint(uint64_t timestamp, uint64_t track); + + // Adds a frame to be output in the file. Returns true on success. + // Inputs: + // data: Pointer to the data + // length: Length of the data + // track_number: Track to add the data to. Value returned by Add track + // functions. + // timestamp: Timestamp of the frame in nanoseconds from 0. + // is_key: Flag telling whether or not this frame is a key frame. + bool AddFrame(const uint8_t* data, uint64_t length, uint64_t track_number, + uint64_t timestamp_ns, bool is_key); + + // Writes a frame of metadata to the output medium; returns true on + // success. + // Inputs: + // data: Pointer to the data + // length: Length of the data + // track_number: Track to add the data to. Value returned by Add track + // functions. + // timecode: Absolute timestamp of the metadata frame, expressed + // in nanosecond units. + // duration: Duration of metadata frame, in nanosecond units. + // + // The metadata frame is written as a block group, with a duration + // sub-element but no reference time sub-elements (indicating that + // it is considered a keyframe, per Matroska semantics). + bool AddMetadata(const uint8_t* data, uint64_t length, uint64_t track_number, + uint64_t timestamp_ns, uint64_t duration_ns); + + // Writes a frame with additional data to the output medium; returns true on + // success. + // Inputs: + // data: Pointer to the data. + // length: Length of the data. + // additional: Pointer to additional data. + // additional_length: Length of additional data. + // add_id: Additional ID which identifies the type of additional data. + // track_number: Track to add the data to. Value returned by Add track + // functions. + // timestamp: Absolute timestamp of the frame, expressed in nanosecond + // units. + // is_key: Flag telling whether or not this frame is a key frame. + bool AddFrameWithAdditional(const uint8_t* data, uint64_t length, + const uint8_t* additional, + uint64_t additional_length, uint64_t add_id, + uint64_t track_number, uint64_t timestamp, + bool is_key); + + // Writes a frame with DiscardPadding to the output medium; returns true on + // success. + // Inputs: + // data: Pointer to the data. + // length: Length of the data. + // discard_padding: DiscardPadding element value. + // track_number: Track to add the data to. Value returned by Add track + // functions. + // timestamp: Absolute timestamp of the frame, expressed in nanosecond + // units. + // is_key: Flag telling whether or not this frame is a key frame. + bool AddFrameWithDiscardPadding(const uint8_t* data, uint64_t length, + int64_t discard_padding, + uint64_t track_number, uint64_t timestamp, + bool is_key); + + // Writes a Frame to the output medium. Chooses the correct way of writing + // the frame (Block vs SimpleBlock) based on the parameters passed. + // Inputs: + // frame: frame object + bool AddGenericFrame(const Frame* frame); + + // Adds a VP8 video track to the segment. Returns the number of the track on + // success, 0 on error. |number| is the number to use for the video track. + // |number| must be >= 0. If |number| == 0 then the muxer will decide on + // the track number. + uint64_t AddVideoTrack(int32_t width, int32_t height, int32_t number); + + // Sets which track to use for the Cues element. Must have added the track + // before calling this function. Returns true on success. |track_number| is + // returned by the Add track functions. + bool CuesTrack(uint64_t track_number); + + // This will force the muxer to create a new Cluster when the next frame is + // added. + void ForceNewClusterOnNextFrame(); + + // Writes out any frames that have not been written out. Finalizes the last + // cluster. May update the size and duration of the segment. May output the + // Cues element. May finalize the SeekHead element. Returns true on success. + bool Finalize(); + + // Returns the Cues object. + Cues* GetCues() { return &cues_; } + + // Returns the Segment Information object. + const SegmentInfo* GetSegmentInfo() const { return &segment_info_; } + SegmentInfo* GetSegmentInfo() { return &segment_info_; } + + // Search the Tracks and return the track that matches |track_number|. + // Returns NULL if there is no track match. + Track* GetTrackByNumber(uint64_t track_number) const; + + // Toggles whether to output a cues element. + void OutputCues(bool output_cues); + + // Toggles whether to write the last frame in each Cluster with Duration. + void AccurateClusterDuration(bool accurate_cluster_duration); + + // Toggles whether to write the Cluster Timecode using exactly 8 bytes. + void UseFixedSizeClusterTimecode(bool fixed_size_cluster_timecode); + + uint64_t cues_track() const { return cues_track_; } + void set_max_cluster_duration(uint64_t max_cluster_duration) { + max_cluster_duration_ = max_cluster_duration; + } + uint64_t max_cluster_duration() const { return max_cluster_duration_; } + void set_max_cluster_size(uint64_t max_cluster_size) { + max_cluster_size_ = max_cluster_size; + } + uint64_t max_cluster_size() const { return max_cluster_size_; } + void set_mode(Mode mode) { mode_ = mode; } + Mode mode() const { return mode_; } + CuesPosition cues_position() const { return cues_position_; } + bool output_cues() const { return output_cues_; } + const SegmentInfo* segment_info() const { return &segment_info_; } + + // Returns true when codec IDs are valid for WebM. + bool DocTypeIsWebm() const; + + private: + // Checks if header information has been output and initialized. If not it + // will output the Segment element and initialize the SeekHead elment and + // Cues elements. + bool CheckHeaderInfo(); + + // Sets |doc_type_version_| based on the current element requirements. + void UpdateDocTypeVersion(); + + // Sets |name| according to how many chunks have been written. |ext| is the + // file extension. |name| must be deleted by the calling app. Returns true + // on success. + bool UpdateChunkName(const char* ext, char** name) const; + + // Returns the maximum offset within the segment's payload. When chunking + // this function is needed to determine offsets of elements within the + // chunked files. Returns -1 on error. + int64_t MaxOffset(); + + // Adds the frame to our frame array. + bool QueueFrame(Frame* frame); + + // Output all frames that are queued. Returns -1 on error, otherwise + // it returns the number of frames written. + int WriteFramesAll(); + + // Output all frames that are queued that have an end time that is less + // then |timestamp|. Returns true on success and if there are no frames + // queued. + bool WriteFramesLessThan(uint64_t timestamp); + + // Outputs the segment header, Segment Information element, SeekHead element, + // and Tracks element to |writer_|. + bool WriteSegmentHeader(); + + // Given a frame with the specified timestamp (nanosecond units) and + // keyframe status, determine whether a new cluster should be + // created, before writing enqueued frames and the frame itself. The + // function returns one of the following values: + // -1 = error: an out-of-order frame was detected + // 0 = do not create a new cluster, and write frame to the existing cluster + // 1 = create a new cluster, and write frame to that new cluster + // 2 = create a new cluster, and re-run test + int TestFrame(uint64_t track_num, uint64_t timestamp_ns, bool key) const; + + // Create a new cluster, using the earlier of the first enqueued + // frame, or the indicated time. Returns true on success. + bool MakeNewCluster(uint64_t timestamp_ns); + + // Checks whether a new cluster needs to be created, and if so + // creates a new cluster. Returns false if creation of a new cluster + // was necessary but creation was not successful. + bool DoNewClusterProcessing(uint64_t track_num, uint64_t timestamp_ns, + bool key); + + // Adjusts Cue Point values (to place Cues before Clusters) so that they + // reflect the correct offsets. + void MoveCuesBeforeClusters(); + + // This function recursively computes the correct cluster offsets (this is + // done to move the Cues before Clusters). It recursively updates the change + // in size (which indicates a change in cluster offset) until no sizes change. + // Parameters: + // diff - indicates the difference in size of the Cues element that needs to + // accounted for. + // index - index in the list of Cues which is currently being adjusted. + // cue_size - sum of size of all the CuePoint elements. + void MoveCuesBeforeClustersHelper(uint64_t diff, int index, + uint64_t* cue_size); + + // Seeds the random number generator used to make UIDs. + unsigned int seed_; + + // WebM elements + Cues cues_; + SeekHead seek_head_; + SegmentInfo segment_info_; + Tracks tracks_; + Chapters chapters_; + Tags tags_; + + // File position offset where the Clusters end. + int64_t cluster_end_offset_; + + // List of clusters. + Cluster** cluster_list_; + + // Number of cluster pointers allocated in the cluster list. + int32_t cluster_list_capacity_; + + // Number of clusters in the cluster list. + int32_t cluster_list_size_; + + // Indicates whether Cues should be written before or after Clusters + CuesPosition cues_position_; + + // Track number that is associated with the cues element for this segment. + uint64_t cues_track_; + + // Tells the muxer to force a new cluster on the next Block. + bool force_new_cluster_; + + // List of stored audio frames. These variables are used to store frames so + // the muxer can follow the guideline "Audio blocks that contain the video + // key frame's timecode should be in the same cluster as the video key frame + // block." + Frame** frames_; + + // Number of frame pointers allocated in the frame list. + int32_t frames_capacity_; + + // Number of frames in the frame list. + int32_t frames_size_; + + // Flag telling if a video track has been added to the segment. + bool has_video_; + + // Flag telling if the segment's header has been written. + bool header_written_; + + // Duration of the last block in nanoseconds. + uint64_t last_block_duration_; + + // Last timestamp in nanoseconds added to a cluster. + uint64_t last_timestamp_; + + // Last timestamp in nanoseconds by track number added to a cluster. + uint64_t last_track_timestamp_[kMaxTrackNumber]; + + // Maximum time in nanoseconds for a cluster duration. This variable is a + // guideline and some clusters may have a longer duration. Default is 30 + // seconds. + uint64_t max_cluster_duration_; + + // Maximum size in bytes for a cluster. This variable is a guideline and + // some clusters may have a larger size. Default is 0 which signifies that + // the muxer will decide the size. + uint64_t max_cluster_size_; + + // The mode that segment is in. If set to |kLive| the writer must not + // seek backwards. + Mode mode_; + + // Flag telling the muxer that a new cue point should be added. + bool new_cuepoint_; + + // TODO(fgalligan): Should we add support for more than one Cues element? + // Flag whether or not the muxer should output a Cues element. + bool output_cues_; + + // Flag whether or not the last frame in each Cluster will have a Duration + // element in it. + bool accurate_cluster_duration_; + + // Flag whether or not to write the Cluster Timecode using exactly 8 bytes. + bool fixed_size_cluster_timecode_; + + // The size of the EBML header, used to validate the header if + // WriteEbmlHeader() is called more than once. + int32_t ebml_header_size_; + + // The file position of the segment's payload. + int64_t payload_pos_; + + // The file position of the element's size. + int64_t size_position_; + + // Current DocTypeVersion (|doc_type_version_|) and that written in + // WriteSegmentHeader(). + // WriteEbmlHeader() will be called from Finalize() if |doc_type_version_| + // differs from |doc_type_version_written_|. + uint32_t doc_type_version_; + uint32_t doc_type_version_written_; + + // Pointer to the writer objects. Not owned by this class. + IMkvWriter* writer_cluster_; + IMkvWriter* writer_cues_; + IMkvWriter* writer_header_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Segment); +}; + +} // namespace mkvmuxer + +#endif // MKVMUXER_MKVMUXER_H_ diff --git a/src/client/bundled/mkvmuxer/mkvmuxertypes.h b/src/client/bundled/mkvmuxer/mkvmuxertypes.h new file mode 100644 index 000000000..e5db12160 --- /dev/null +++ b/src/client/bundled/mkvmuxer/mkvmuxertypes.h @@ -0,0 +1,28 @@ +// Copyright (c) 2012 The WebM project authors. All Rights Reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. An additional intellectual property rights grant can be found +// in the file PATENTS. All contributing project authors may +// be found in the AUTHORS file in the root of the source tree. + +#ifndef MKVMUXER_MKVMUXERTYPES_H_ +#define MKVMUXER_MKVMUXERTYPES_H_ + +namespace mkvmuxer { +typedef unsigned char uint8; +typedef short int16; +typedef int int32; +typedef unsigned int uint32; +typedef long long int64; +typedef unsigned long long uint64; +} // namespace mkvmuxer + +// Copied from Chromium basictypes.h +// A macro to disallow the copy constructor and operator= functions +// This should be used in the private: declarations for a class +#define LIBWEBM_DISALLOW_COPY_AND_ASSIGN(TypeName) \ + TypeName(const TypeName&); \ + void operator=(const TypeName&) + +#endif // MKVMUXER_MKVMUXERTYPES_HPP_ diff --git a/src/client/bundled/mkvmuxer/mkvmuxerutil.cc b/src/client/bundled/mkvmuxer/mkvmuxerutil.cc new file mode 100644 index 000000000..6e79cba49 --- /dev/null +++ b/src/client/bundled/mkvmuxer/mkvmuxerutil.cc @@ -0,0 +1,648 @@ +// Copyright (c) 2012 The WebM project authors. All Rights Reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. An additional intellectual property rights grant can be found +// in the file PATENTS. All contributing project authors may +// be found in the AUTHORS file in the root of the source tree. + +#include "mkvmuxer/mkvmuxerutil.h" + +#ifdef __ANDROID__ +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include "common/webmids.h" +#include "mkvmuxer/mkvmuxer.h" + +namespace mkvmuxer { + +namespace { + +// Date elements are always 8 octets in size. +const int kDateElementSize = 8; + +uint64 WriteBlock(IMkvWriter* writer, const Frame* const frame, int64 timecode, + uint64 timecode_scale) { + uint64 block_additional_elem_size = 0; + uint64 block_addid_elem_size = 0; + uint64 block_more_payload_size = 0; + uint64 block_more_elem_size = 0; + uint64 block_additions_payload_size = 0; + uint64 block_additions_elem_size = 0; + if (frame->additional()) { + block_additional_elem_size = + EbmlElementSize(libwebm::kMkvBlockAdditional, frame->additional(), + frame->additional_length()); + block_addid_elem_size = EbmlElementSize( + libwebm::kMkvBlockAddID, static_cast(frame->add_id())); + + block_more_payload_size = + block_addid_elem_size + block_additional_elem_size; + block_more_elem_size = + EbmlMasterElementSize(libwebm::kMkvBlockMore, block_more_payload_size) + + block_more_payload_size; + block_additions_payload_size = block_more_elem_size; + block_additions_elem_size = + EbmlMasterElementSize(libwebm::kMkvBlockAdditions, + block_additions_payload_size) + + block_additions_payload_size; + } + + uint64 discard_padding_elem_size = 0; + if (frame->discard_padding() != 0) { + discard_padding_elem_size = + EbmlElementSize(libwebm::kMkvDiscardPadding, + static_cast(frame->discard_padding())); + } + + const uint64 reference_block_timestamp = + frame->reference_block_timestamp() / timecode_scale; + uint64 reference_block_elem_size = 0; + if (!frame->is_key()) { + reference_block_elem_size = + EbmlElementSize(libwebm::kMkvReferenceBlock, reference_block_timestamp); + } + + const uint64 duration = frame->duration() / timecode_scale; + uint64 block_duration_elem_size = 0; + if (duration > 0) + block_duration_elem_size = + EbmlElementSize(libwebm::kMkvBlockDuration, duration); + + const uint64 block_payload_size = 4 + frame->length(); + const uint64 block_elem_size = + EbmlMasterElementSize(libwebm::kMkvBlock, block_payload_size) + + block_payload_size; + + const uint64 block_group_payload_size = + block_elem_size + block_additions_elem_size + block_duration_elem_size + + discard_padding_elem_size + reference_block_elem_size; + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvBlockGroup, + block_group_payload_size)) { + return 0; + } + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvBlock, block_payload_size)) + return 0; + + if (WriteUInt(writer, frame->track_number())) + return 0; + + if (SerializeInt(writer, timecode, 2)) + return 0; + + // For a Block, flags is always 0. + if (SerializeInt(writer, 0, 1)) + return 0; + + if (writer->Write(frame->frame(), static_cast(frame->length()))) + return 0; + + if (frame->additional()) { + if (!WriteEbmlMasterElement(writer, libwebm::kMkvBlockAdditions, + block_additions_payload_size)) { + return 0; + } + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvBlockMore, + block_more_payload_size)) + return 0; + + if (!WriteEbmlElement(writer, libwebm::kMkvBlockAddID, + static_cast(frame->add_id()))) + return 0; + + if (!WriteEbmlElement(writer, libwebm::kMkvBlockAdditional, + frame->additional(), frame->additional_length())) { + return 0; + } + } + + if (frame->discard_padding() != 0 && + !WriteEbmlElement(writer, libwebm::kMkvDiscardPadding, + static_cast(frame->discard_padding()))) { + return false; + } + + if (!frame->is_key() && + !WriteEbmlElement(writer, libwebm::kMkvReferenceBlock, + reference_block_timestamp)) { + return false; + } + + if (duration > 0 && + !WriteEbmlElement(writer, libwebm::kMkvBlockDuration, duration)) { + return false; + } + return EbmlMasterElementSize(libwebm::kMkvBlockGroup, + block_group_payload_size) + + block_group_payload_size; +} + +uint64 WriteSimpleBlock(IMkvWriter* writer, const Frame* const frame, + int64 timecode) { + if (WriteID(writer, libwebm::kMkvSimpleBlock)) + return 0; + + const int32 size = static_cast(frame->length()) + 4; + if (WriteUInt(writer, size)) + return 0; + + if (WriteUInt(writer, static_cast(frame->track_number()))) + return 0; + + if (SerializeInt(writer, timecode, 2)) + return 0; + + uint64 flags = 0; + if (frame->is_key()) + flags |= 0x80; + + if (SerializeInt(writer, flags, 1)) + return 0; + + if (writer->Write(frame->frame(), static_cast(frame->length()))) + return 0; + + return GetUIntSize(libwebm::kMkvSimpleBlock) + GetCodedUIntSize(size) + 4 + + frame->length(); +} + +} // namespace + +int32 GetCodedUIntSize(uint64 value) { + if (value < 0x000000000000007FULL) + return 1; + else if (value < 0x0000000000003FFFULL) + return 2; + else if (value < 0x00000000001FFFFFULL) + return 3; + else if (value < 0x000000000FFFFFFFULL) + return 4; + else if (value < 0x00000007FFFFFFFFULL) + return 5; + else if (value < 0x000003FFFFFFFFFFULL) + return 6; + else if (value < 0x0001FFFFFFFFFFFFULL) + return 7; + return 8; +} + +int32 GetUIntSize(uint64 value) { + if (value < 0x0000000000000100ULL) + return 1; + else if (value < 0x0000000000010000ULL) + return 2; + else if (value < 0x0000000001000000ULL) + return 3; + else if (value < 0x0000000100000000ULL) + return 4; + else if (value < 0x0000010000000000ULL) + return 5; + else if (value < 0x0001000000000000ULL) + return 6; + else if (value < 0x0100000000000000ULL) + return 7; + return 8; +} + +int32 GetIntSize(int64 value) { + // Doubling the requested value ensures positive values with their high bit + // set are written with 0-padding to avoid flipping the signedness. + const uint64 v = (value < 0) ? value ^ -1LL : value; + return GetUIntSize(2 * v); +} + +uint64 EbmlMasterElementSize(uint64 type, uint64 value) { + // Size of EBML ID + int32 ebml_size = GetUIntSize(type); + + // Datasize + ebml_size += GetCodedUIntSize(value); + + return ebml_size; +} + +uint64 EbmlElementSize(uint64 type, int64 value) { + // Size of EBML ID + int32 ebml_size = GetUIntSize(type); + + // Datasize + ebml_size += GetIntSize(value); + + // Size of Datasize + ebml_size++; + + return ebml_size; +} + +uint64 EbmlElementSize(uint64 type, uint64 value) { + return EbmlElementSize(type, value, 0); +} + +uint64 EbmlElementSize(uint64 type, uint64 value, uint64 fixed_size) { + // Size of EBML ID + int32 ebml_size = GetUIntSize(type); + + // Datasize + ebml_size += (fixed_size > 0) ? fixed_size : GetUIntSize(value); + + // Size of Datasize + ebml_size++; + + return ebml_size; +} + +uint64 EbmlElementSize(uint64 type, float /* value */) { + // Size of EBML ID + uint64 ebml_size = GetUIntSize(type); + + // Datasize + ebml_size += sizeof(float); + + // Size of Datasize + ebml_size++; + + return ebml_size; +} + +uint64 EbmlElementSize(uint64 type, const char* value) { + if (!value) + return 0; + + // Size of EBML ID + uint64 ebml_size = GetUIntSize(type); + + // Datasize + ebml_size += strlen(value); + + // Size of Datasize + ebml_size++; + + return ebml_size; +} + +uint64 EbmlElementSize(uint64 type, const uint8* value, uint64 size) { + if (!value) + return 0; + + // Size of EBML ID + uint64 ebml_size = GetUIntSize(type); + + // Datasize + ebml_size += size; + + // Size of Datasize + ebml_size += GetCodedUIntSize(size); + + return ebml_size; +} + +uint64 EbmlDateElementSize(uint64 type) { + // Size of EBML ID + uint64 ebml_size = GetUIntSize(type); + + // Datasize + ebml_size += kDateElementSize; + + // Size of Datasize + ebml_size++; + + return ebml_size; +} + +int32 SerializeInt(IMkvWriter* writer, int64 value, int32 size) { + if (!writer || size < 1 || size > 8) + return -1; + + for (int32 i = 1; i <= size; ++i) { + const int32 byte_count = size - i; + const int32 bit_count = byte_count * 8; + + const int64 bb = value >> bit_count; + const uint8 b = static_cast(bb); + + const int32 status = writer->Write(&b, 1); + + if (status < 0) + return status; + } + + return 0; +} + +int32 SerializeFloat(IMkvWriter* writer, float f) { + if (!writer) + return -1; + + assert(sizeof(uint32) == sizeof(float)); + // This union is merely used to avoid a reinterpret_cast from float& to + // uint32& which will result in violation of strict aliasing. + union U32 { + uint32 u32; + float f; + } value; + value.f = f; + + for (int32 i = 1; i <= 4; ++i) { + const int32 byte_count = 4 - i; + const int32 bit_count = byte_count * 8; + + const uint8 byte = static_cast
Drawpile is a drawing program that lets you share a canvas with other users in real time.
Feature highlights: