├── .clang-format ├── .dockerignore ├── .github └── workflows │ └── release.yaml ├── .gitignore ├── .gitmodules ├── Dockerfile.osx ├── Dockerfile.ubuntu-bionic ├── Dockerfile.ubuntu-xenial ├── Dockerfile.win32 ├── Dockerfile.win64 ├── Dockerfile.x11 ├── Dockerfile.x11_32 ├── LICENSE ├── README.md ├── SConstruct ├── build_gdnative.sh ├── build_gdnative.sh.example ├── build_gdnative_test.sh ├── darwin_sdk └── .gitignore ├── renamer.py ├── src ├── gdnative_videodecoder.c ├── gdnative_videodecoder.h ├── linked_list.h ├── packet_queue.h └── set.h └── test ├── .import ├── icon.png-487276ed1e3a0c39cad0279d744ee560.md5 └── icon.png-487276ed1e3a0c39cad0279d744ee560.stex ├── Node2D.tscn ├── World.gd ├── World.tscn ├── addons └── videodecoder.gdnlib ├── default_env.tres ├── icon.png ├── icon.png.import ├── project.godot └── test_samples ├── file_example_WEBM_480_900KB.webm ├── out8.webm ├── out9.webm └── sources.txt /.clang-format: -------------------------------------------------------------------------------- 1 | # Commented out parameters are those with the same value as base LLVM style 2 | # We can uncomment them if we want to change their value, or enforce the 3 | # chosen value in case the base style changes (last sync: Clang 5.0.0). 4 | --- 5 | ### General config, applies to all languages ### 6 | BasedOnStyle: LLVM 7 | AccessModifierOffset: -4 8 | AlignAfterOpenBracket: DontAlign 9 | # AlignConsecutiveAssignments: false 10 | # AlignConsecutiveDeclarations: false 11 | # AlignEscapedNewlines: Right 12 | # AlignOperands: true 13 | AlignTrailingComments: false 14 | AllowAllParametersOfDeclarationOnNextLine: false 15 | # AllowShortBlocksOnASingleLine: false 16 | AllowShortCaseLabelsOnASingleLine: true 17 | AllowShortFunctionsOnASingleLine: Inline 18 | AllowShortIfStatementsOnASingleLine: true 19 | # AllowShortLoopsOnASingleLine: false 20 | # AlwaysBreakAfterDefinitionReturnType: None 21 | # AlwaysBreakAfterReturnType: None 22 | # AlwaysBreakBeforeMultilineStrings: false 23 | # AlwaysBreakTemplateDeclarations: false 24 | # BinPackArguments: true 25 | # BinPackParameters: true 26 | # BraceWrapping: 27 | # AfterClass: false 28 | # AfterControlStatement: false 29 | # AfterEnum: false 30 | # AfterFunction: false 31 | # AfterNamespace: false 32 | # AfterObjCDeclaration: false 33 | # AfterStruct: false 34 | # AfterUnion: false 35 | # BeforeCatch: false 36 | # BeforeElse: false 37 | # IndentBraces: false 38 | # SplitEmptyFunction: true 39 | # SplitEmptyRecord: true 40 | # SplitEmptyNamespace: true 41 | # BreakBeforeBinaryOperators: None 42 | # BreakBeforeBraces: Attach 43 | # BreakBeforeInheritanceComma: false 44 | BreakBeforeTernaryOperators: false 45 | # BreakConstructorInitializersBeforeComma: false 46 | BreakConstructorInitializers: AfterColon 47 | # BreakStringLiterals: true 48 | ColumnLimit: 0 49 | # CommentPragmas: '^ IWYU pragma:' 50 | # CompactNamespaces: false 51 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 52 | ConstructorInitializerIndentWidth: 8 53 | ContinuationIndentWidth: 8 54 | Cpp11BracedListStyle: false 55 | # DerivePointerAlignment: false 56 | # DisableFormat: false 57 | # ExperimentalAutoDetectBinPacking: false 58 | # FixNamespaceComments: true 59 | # ForEachMacros: 60 | # - foreach 61 | # - Q_FOREACH 62 | # - BOOST_FOREACH 63 | IncludeCategories: 64 | - Regex: '".*"' 65 | Priority: 1 66 | - Regex: '^<.*\.h>' 67 | Priority: 2 68 | - Regex: '^<.*' 69 | Priority: 3 70 | # IncludeIsMainRegex: '(Test)?$' 71 | IndentCaseLabels: true 72 | IndentWidth: 4 73 | # IndentWrappedFunctionNames: false 74 | # JavaScriptQuotes: Leave 75 | # JavaScriptWrapImports: true 76 | # KeepEmptyLinesAtTheStartOfBlocks: true 77 | # MacroBlockBegin: '' 78 | # MacroBlockEnd: '' 79 | # MaxEmptyLinesToKeep: 1 80 | # NamespaceIndentation: None 81 | # PenaltyBreakAssignment: 2 82 | # PenaltyBreakBeforeFirstCallParameter: 19 83 | # PenaltyBreakComment: 300 84 | # PenaltyBreakFirstLessLess: 120 85 | # PenaltyBreakString: 1000 86 | # PenaltyExcessCharacter: 1000000 87 | # PenaltyReturnTypeOnItsOwnLine: 60 88 | # PointerAlignment: Right 89 | # ReflowComments: true 90 | # SortIncludes: true 91 | # SortUsingDeclarations: true 92 | # SpaceAfterCStyleCast: false 93 | # SpaceAfterTemplateKeyword: true 94 | # SpaceBeforeAssignmentOperators: true 95 | # SpaceBeforeParens: ControlStatements 96 | # SpaceInEmptyParentheses: false 97 | # SpacesBeforeTrailingComments: 1 98 | # SpacesInAngles: false 99 | # SpacesInContainerLiterals: true 100 | # SpacesInCStyleCastParentheses: false 101 | # SpacesInParentheses: false 102 | # SpacesInSquareBrackets: false 103 | TabWidth: 4 104 | UseTab: Always 105 | --- 106 | ### C++ specific config ### 107 | Language: Cpp 108 | Standard: Cpp03 109 | --- 110 | ### ObjC specific config ### 111 | Language: ObjC 112 | ObjCBlockIndentWidth: 4 113 | # ObjCSpaceAfterProperty: false 114 | # ObjCSpaceBeforeProtocolList: true 115 | --- 116 | ### Java specific config ### 117 | Language: Java 118 | # BreakAfterJavaFieldAnnotations: false 119 | ... 120 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | bin/* 2 | thirdparty/* 3 | test 4 | 5 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - '*' # Push events to matching v*, i.e. v1.0, v20.15.10 6 | 7 | jobs: 8 | build: 9 | name: Upload Release Assets 10 | 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | with: 16 | submodules: recursive 17 | - name: Build for windows/x11 18 | run: | 19 | PLATFORMS=win64,x11,win32,x11_32 ./build_gdnative.sh 20 | zip -r target.zip target 21 | - name: Create Release 22 | id: create_release 23 | uses: actions/create-release@v1 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | with: 27 | tag_name: ${{ github.ref }} 28 | release_name: Release ${{ github.ref }} 29 | draft: false 30 | prerelease: true 31 | - name: Upload Release Asset 32 | id: upload-release-asset 33 | uses: actions/upload-release-asset@v1 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | with: 37 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 38 | asset_path: ./target.zip 39 | asset_name: target.zip 40 | asset_content_type: application/zip` 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .sconsign.dblite 3 | *.o 4 | *.os 5 | bin/ 6 | thirdparty/ 7 | .import/ 8 | .DS_Store 9 | .test/addons/bin 10 | *.obj 11 | *.dll 12 | /target 13 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "godot_include"] 2 | path = godot_include 3 | url = https://github.com/GodotNativeTools/godot_headers.git 4 | [submodule "ffmpeg-static"] 5 | path = ffmpeg-static 6 | url = https://github.com/jamie-pate/ffmpeg-static.git 7 | [submodule "build-containers"] 8 | path = build-containers 9 | url = https://github.com/godotengine/build-containers.git 10 | -------------------------------------------------------------------------------- /Dockerfile.osx: -------------------------------------------------------------------------------- 1 | FROM godot-videodecoder-ubuntu-bionic:latest 2 | 3 | WORKDIR /opt/godot-videodecoder/ffmpeg-static 4 | COPY ./ffmpeg-static . 5 | 6 | ENV OSXCROSS_ROOT=/opt/osxcross 7 | ENV OSXCROSS_BIN_DIR=$OSXCROSS_ROOT/target/bin 8 | 9 | # download dependencies 10 | RUN ./build.sh -d -p darwin 11 | 12 | ARG JOBS=1 13 | ENV JOBS=$JOBS 14 | ENV THIRDPARTY_DIR=/opt/godot-videodecoder/thirdparty 15 | RUN ./build.sh -B -p darwin -T "$THIRDPARTY_DIR/osx" -j $JOBS 16 | 17 | ENV FINAL_TARGET_DIR=/opt/target 18 | ENV PLUGIN_BIN_DIR=/opt/godot-videodecoder/bin 19 | 20 | WORKDIR /opt/godot-videodecoder 21 | 22 | COPY . . 23 | RUN scons -c platform=osx 24 | RUN scons platform=osx toolchainbin=${OSXCROSS_BIN_DIR} prefix="${FINAL_TARGET_DIR}" 25 | 26 | -------------------------------------------------------------------------------- /Dockerfile.ubuntu-bionic: -------------------------------------------------------------------------------- 1 | FROM ubuntu:bionic-20200403 2 | 3 | 4 | # build environment for osxcross 5 | 6 | # download XCode (7.3.1) from https://developer.apple.com/download/more/?name=Xcode%207.3.1 7 | # extract the sdk tarball using the following instructions: 8 | # https://github.com/tpoechtrager/osxcross#packing-the-sdk-on-linux---method-2-works-up-to-xcode-73 9 | ARG XCODE_SDK= 10 | 11 | ENV DEBIAN_FRONTEND=noninteractive 12 | 13 | RUN apt-get update && \ 14 | apt-get install -y cmake git patch clang \ 15 | libbz2-dev fuse libfuse-dev libxml2-dev gcc g++ \ 16 | zlib1g-dev libmpc-dev libmpfr-dev libgmp-dev libc++-dev \ 17 | libssl-dev curl bc wget \ 18 | autoconf automake libtool make yasm nasm \ 19 | scons mingw-w64 mingw-w64-tools 20 | 21 | # use posix variant of mingw 22 | RUN update-alternatives --set x86_64-w64-mingw32-gcc /usr/bin/x86_64-w64-mingw32-gcc-posix 23 | RUN update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix 24 | 25 | RUN git clone --depth=1 https://github.com/tpoechtrager/osxcross.git /opt/osxcross 26 | 27 | # TODO: ln this from a volume instead? 28 | COPY ./darwin_sdk/* /opt/osxcross/tarballs/ 29 | 30 | RUN cmake --version 31 | RUN [ -z "$XCODE_SDK" ] || (cd /opt/osxcross && UNATTENDED=1 ./build.sh) 32 | RUN [ -z "$XCODE_SDK" ] || (echo "building gcc"; cd /opt/osxcross && UNATTENDED=1 ./build_gcc.sh) 33 | 34 | WORKDIR /opt/godot-videodecoder/ 35 | -------------------------------------------------------------------------------- /Dockerfile.ubuntu-xenial: -------------------------------------------------------------------------------- 1 | FROM ubuntu:xenial-20200326 2 | 3 | #build environment for ubuntu with older glibc 4 | ENV DEBIAN_FRONTEND=noninteractive 5 | ARG JOBS 6 | 7 | # from Dockerfile-ubuntu-64 8 | #RUN apt-get update && \ 9 | # apt-get -y install wget && \ 10 | # cd /root && \ 11 | # wget -O- 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x1E9377A2BA9EF27F' | apt-key add - && \ 12 | # wget -O- 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x8E51A6D660CD88D67D65221D90BD7EACED8E640A' | apt-key add - && \ 13 | # echo 'deb http://ppa.launchpad.net/ubuntu-toolchain-r/test/ubuntu trusty main' >> /etc/apt/sources.list && \ 14 | # echo 'deb http://ppa.launchpad.net/mc3man/trusty-media/ubuntu trusty main' >> /etc/apt/sources.list 15 | 16 | # godot deps 17 | RUN apt update && \ 18 | apt install -y build-essential scons pkg-config libx11-dev libxcursor-dev libxinerama-dev \ 19 | libgl1-mesa-dev libglu-dev libasound2-dev libpulse-dev libudev-dev libxi-dev libxrandr-dev yasm 20 | # ffmpeg deps: may be some duplicates 21 | RUN apt install -y autoconf \ 22 | build-essential curl tar pkg-config \ 23 | automake \ 24 | build-essential \ 25 | cmake \ 26 | frei0r-plugins-dev \ 27 | gawk \ 28 | libass-dev \ 29 | libfreetype6-dev \ 30 | libopencore-amrnb-dev \ 31 | libopencore-amrwb-dev \ 32 | libsdl1.2-dev \ 33 | libspeex-dev \ 34 | libssl-dev \ 35 | libtheora-dev \ 36 | libtool \ 37 | libva-dev \ 38 | libvdpau-dev \ 39 | libvo-amrwbenc-dev \ 40 | libvorbis-dev \ 41 | libwebp-dev \ 42 | libxcb1-dev \ 43 | libxcb-shm0-dev \ 44 | libxcb-xfixes0-dev \ 45 | libxvidcore-dev \ 46 | pkg-config \ 47 | texi2html \ 48 | zlib1g-dev \ 49 | mingw-w64-tools \ 50 | wget 51 | 52 | RUN apt install -y ocl-icd-opencl-dev 53 | -------------------------------------------------------------------------------- /Dockerfile.win32: -------------------------------------------------------------------------------- 1 | FROM godot-videodecoder-ubuntu-bionic:latest 2 | 3 | WORKDIR /opt/godot-videodecoder/ffmpeg-static 4 | COPY ./ffmpeg-static . 5 | # download dependencies 6 | RUN ./build.sh -d -p win32 7 | 8 | ARG JOBS=1 9 | ENV JOBS=$JOBS 10 | ENV THIRDPARTY_DIR=/opt/godot-videodecoder/thirdparty 11 | RUN ./build.sh -B -p win32 -T "$THIRDPARTY_DIR/win32" -j $JOBS 12 | 13 | ENV FINAL_TARGET_DIR=/opt/target 14 | ENV PLUGIN_BIN_DIR=/opt/godot-videodecoder/bin 15 | 16 | WORKDIR /opt/godot-videodecoder 17 | 18 | COPY . . 19 | RUN scons -c platform=win32 20 | RUN scons platform=win32 prefix="${FINAL_TARGET_DIR}" 21 | 22 | -------------------------------------------------------------------------------- /Dockerfile.win64: -------------------------------------------------------------------------------- 1 | FROM godot-videodecoder-ubuntu-bionic:latest 2 | 3 | WORKDIR /opt/godot-videodecoder/ffmpeg-static 4 | COPY ./ffmpeg-static . 5 | # download dependencies 6 | RUN ./build.sh -d -p windows 7 | 8 | ARG JOBS=1 9 | ENV JOBS=$JOBS 10 | ENV THIRDPARTY_DIR=/opt/godot-videodecoder/thirdparty 11 | RUN ./build.sh -B -p windows -T "$THIRDPARTY_DIR/win64" -j $JOBS 12 | 13 | ENV FINAL_TARGET_DIR=/opt/target 14 | ENV PLUGIN_BIN_DIR=/opt/godot-videodecoder/bin 15 | 16 | WORKDIR /opt/godot-videodecoder 17 | 18 | COPY . . 19 | RUN scons -c platform=windows 20 | RUN scons platform=windows prefix="${FINAL_TARGET_DIR}" 21 | -------------------------------------------------------------------------------- /Dockerfile.x11: -------------------------------------------------------------------------------- 1 | FROM godot-videodecoder-ubuntu-xenial:latest 2 | 3 | WORKDIR /opt/godot-videodecoder/ffmpeg-static 4 | COPY ./ffmpeg-static . 5 | # download dependencies 6 | RUN ./build.sh -d 7 | 8 | ARG JOBS=1 9 | ENV JOBS=$JOBS 10 | ENV THIRDPARTY_DIR=/opt/godot-videodecoder/thirdparty 11 | RUN ./build.sh -B -T "$THIRDPARTY_DIR/x11" -j $JOBS 12 | 13 | ENV FINAL_TARGET_DIR=/opt/target 14 | ENV PLUGIN_BIN_DIR=/opt/godot-videodecoder/bin 15 | 16 | WORKDIR /opt/godot-videodecoder 17 | 18 | COPY . . 19 | RUN scons -c platform=x11 20 | RUN scons platform=x11 prefix="${FINAL_TARGET_DIR}" 21 | RUN ldd /opt/target/x11/libgdnative_videodecoder.so 22 | -------------------------------------------------------------------------------- /Dockerfile.x11_32: -------------------------------------------------------------------------------- 1 | FROM godot-videodecoder-ubuntu-xenial:latest 2 | RUN dpkg --add-architecture i386; apt update 3 | 4 | RUN apt install -y libc6-dev-i386 libgl1-mesa-dev:i386 5 | 6 | WORKDIR /opt/godot-videodecoder/ffmpeg-static 7 | COPY ./ffmpeg-static . 8 | # download dependencies 9 | RUN ./build.sh -d -p x11_32 10 | 11 | ARG JOBS=1 12 | ENV JOBS=$JOBS 13 | ENV THIRDPARTY_DIR=/opt/godot-videodecoder/thirdparty 14 | RUN ./build.sh -B -p x11_32 -T "$THIRDPARTY_DIR/x11_32" -j $JOBS 15 | 16 | ENV FINAL_TARGET_DIR=/opt/target 17 | ENV PLUGIN_BIN_DIR=/opt/godot-videodecoder/bin 18 | 19 | WORKDIR /opt/godot-videodecoder 20 | 21 | COPY . . 22 | RUN scons -c platform=x11_32 23 | RUN scons platform=x11_32 prefix="${FINAL_TARGET_DIR}" 24 | RUN ldd /opt/target/x11_32/libgdnative_videodecoder.so 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Anish Bhobe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Godot Video Decoder 2 | 3 | GDNative Video Decoder library for [Godot Engine](https://godotengine.org), 4 | using the [FFmpeg](https://ffmpeg.org) library for codecs. 5 | 6 | **A GSoC 2018 Project** 7 | 8 | This project is set up so that a game developer can build x11, windows and osx plugin libraries with a single script. The build enviroment has been dockerized for portability. Building has been tested on linux and windows 10 pro with WSL2. 9 | 10 | The most difficult part of building the plugin libraries is extracting the macos sdk from the XCode download since it [can't be distributed directly](https://www.apple.com/legal/sla/docs/xcode.pdf). 11 | 12 | **Releases** 13 | 14 | Release builds should automatically be available [here](/releases/latest) on github. 15 | 16 | **Media Support** 17 | 18 | The current dockerized ffmpeg build supports VP9 decoding only. Support for other decoders could be added, PRs are welcome. 19 | Patent encumbered codecs like [h264/h265](https://www.mpegla.com/wp-content/uploads/avcweb.pdf) will always be missing in the automatic builds due to copyright issues and the [cost of distributing pre-built binaries](https://jina-liu.medium.com/settle-your-questions-about-h-264-license-cost-once-and-for-all-hopefully-a058c2149256#5e65). 20 | 21 | ## Instructions to build with docker 22 | 23 | 1. Add the repository as a submodule or clone the repository somewhere and initialize submodules. 24 | 25 | ``` 26 | git submodule add https://github.com/jamie-pate/godot-videodecoder.git contrib/godot-videodecoder 27 | git submodule update --init --recursive 28 | ``` 29 | 30 | or 31 | 32 | ``` 33 | git clone https://github.com/jamie-pate/godot-videodecoder.git godot-videodecoder 34 | cd godot-videodecoder 35 | git submodule update --init --recursive 36 | ``` 37 | 38 | 2. Copy the `build_gdnative.sh.example` to your project and adjust the paths inside. 39 | 40 | * `cp contrib/godot-videodecoder/build_gdnative.sh.example ./build_gdnative.sh`, vi `./build_gdnative.sh` 41 | * `chmod +x ./build_gdnative.sh` if needed 42 | 43 | 4. [Install docker](https://docs.docker.com/get-docker/) 44 | 45 | 5. Extract MacOSX sdk from the XCode 46 | 47 | * For osx you must download XCode 7 and extract/generate MacOSX10.11.sdk.tar.gz and copy it to ./darwin_sdk/ by following these instructions: https://github.com/tpoechtrager/osxcross#packaging-the-sdk 48 | * NOTE: for darwin15 support use: https://developer.apple.com/download/more/?name=Xcode%207.3.1 49 | * To use a different MacOSX*.*.sdk.tar.gz sdk set the XCODE_SDK environment variable. 50 | * e.g. `XCODE_SDK=$PWD/darwin_sdk/MacOSX10.15.sdk.tar.gz ./build_gdnative.sh` 51 | 52 | 5. run `build_gdnative.sh` (Be sure to [add your user to the `docker` group](https://docs.docker.com/engine/install/linux-postinstall/) or you will have to run as `sudo` (which is bad)) 53 | 54 | TODO: 55 | 56 | * instructions for running the test project 57 | * Add a benchmark to the test project 58 | * Input for additional ffmpeg flags/deps 59 | * Re-enable additional media formats with the ability to build only a subset 60 | * OSX build in releases via github actions 61 | -------------------------------------------------------------------------------- /SConstruct: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | from glob import glob 5 | 6 | opts = Variables() 7 | 8 | opts.Add(BoolVariable('debug','debug build',True)) 9 | opts.Add(BoolVariable('test','copy output to test project',True)) 10 | opts.Add(EnumVariable('platform','can be osx, linux (x11) or windows (win64)','',('osx','x11','win64','win32','x11_32'), 11 | map={'linux':'x11','windows':'win64'})) 12 | opts.Add(PathVariable('toolchainbin', 'Path to the cross compiler toolchain bin directory. Only needed cross compiling and the toolchain isn\'t installed.', '', PathVariable.PathAccept)) 13 | opts.Add(PathVariable('thirdparty', 'Path containing the ffmpeg libs', 'thirdparty', PathVariable.PathAccept)) 14 | opts.Add(PathVariable('prefix', 'Path where the output lib will be installed', '', PathVariable.PathAccept)) 15 | opts.Add(EnumVariable('darwinver', 'Darwin SDK version. (if cross compiling from linux to osx)', '15', [str(v) for v in range(11, 19 + 1)])) 16 | 17 | #probably a better way to do this instead of creating Enviroment() twice 18 | early_env=Environment(variables=opts, BUILDERS={}) 19 | lib_prefix = early_env['thirdparty'] + '/' + early_env['platform'] 20 | 21 | msvc_build = os.name == 'nt' 22 | if msvc_build: 23 | lib_path = lib_prefix + '/bin' 24 | else: 25 | lib_path = lib_prefix + '/lib' 26 | include_path = lib_prefix + '/include' 27 | # probably a better way to do this too (pass $TOOL_PREFIX) 28 | # PWD is not present in windows 29 | pwd = os.environ.get('PWD') or os.getcwd() 30 | 31 | osx_renamer = Builder(action = './renamer.py ' + pwd + '/' + lib_path + '/ @loader_path/ "$TOOL_PREFIX" $SOURCE', ) 32 | 33 | env = Environment(variables=opts, BUILDERS={'OSXRename':osx_renamer}, CFLAGS='/WX' if msvc_build else '-std=gnu11') 34 | env.Append(TARGET_ARCH='i386' if env['platform'].endswith('32') else 'x86_64') 35 | 36 | if env['toolchainbin']: 37 | env.PrependENVPath('PATH', env['toolchainbin']) 38 | output_path = '#bin/' + env['platform'] + '/' 39 | 40 | if env['debug'] and not msvc_build: 41 | env.Append(CPPFLAGS=['-g']) 42 | 43 | env.Append(LIBPATH=[lib_path]) 44 | if env['platform'] == 'x11': 45 | env.Append(RPATH=env.Literal('\$$ORIGIN')) 46 | # statically link glibc 47 | env.Append(LIBS=[File('/usr/lib/x86_64-linux-gnu/libc_nonshared.a')]) 48 | if env['platform'] == 'x11_32': 49 | env.Append(RPATH=env.Literal('\$$ORIGIN')) 50 | # statically link glibc 51 | env.Append(LIBS=[File('/usr/lib32/libc_nonshared.a')]) 52 | env.Append(CFLAGS=['-m32']) 53 | env.Append(LINKFLAGS=['-m32']) 54 | if env['platform'] == 'win32': 55 | env.Append(LIBS=['pthread']) 56 | env.Append(LINKFLAGS=['-static-libgcc']) 57 | 58 | env.Append(CPPPATH=['#' + include_path + '/']) 59 | env.Append(CPPPATH=['#godot_include']) 60 | 61 | tool_prefix = '' 62 | if os.name == 'posix' and env['platform'] == 'win64': 63 | tool_prefix = "x86_64-w64-mingw32-" 64 | env['SHLIBSUFFIX'] = '.dll' 65 | env.Append(CPPDEFINES='WIN32') 66 | if os.name == 'posix' and env['platform'] == 'win32': 67 | tool_prefix = "i686-w64-mingw32-" 68 | env['SHLIBSUFFIX'] = '.dll' 69 | env.Append(CPPDEFINES='WIN32') 70 | if os.name == 'posix' and env['platform'] == 'osx': 71 | tool_prefix = 'x86_64-apple-darwin' + env['darwinver'] + '-' 72 | if (os.getenv("OSXCROSS_PREFIX")): 73 | tool_prefix = os.getenv('OSXCROSS_PREFIX') 74 | env['SHLIBSUFFIX'] = '.dylib' 75 | 76 | globs = { 77 | 'x11': '*.so.[0-9]*', 78 | 'win64': '../bin/*-[0-9]*.dll', 79 | 'osx': '*.[0-9]*.dylib', 80 | 'x11_32': '*.so.[0-9]*', 81 | 'win32': '../bin/*-[0-9]*.dll', 82 | } 83 | 84 | ffmpeg_dylibs = glob(lib_path + '/' + globs[env['platform']]) 85 | 86 | if env['platform'].startswith('win') and tool_prefix: 87 | # mingw needs libwinpthread-1.dll which should be here. (remove trailing '-' from tool_prefix) 88 | winpthread = '/usr/%s/lib/libwinpthread-1.dll' % tool_prefix[:-1] 89 | ffmpeg_dylibs.append(winpthread) 90 | 91 | installed_dylib = [] 92 | for dylib in ffmpeg_dylibs: 93 | installed_dylib.append(env.Install(output_path,dylib)) 94 | 95 | if tool_prefix: 96 | env['CC'] = tool_prefix + 'gcc' 97 | env['AS'] = tool_prefix + 'as' 98 | env['CXX'] = tool_prefix + 'g++' 99 | env['AR'] = tool_prefix + 'gcc-ar' 100 | env['RANLIB'] = tool_prefix + 'gcc-ranlib' 101 | if env['toolchainbin']: 102 | # there's probably a better way to pass the PATH to the renamer script 103 | env['TOOL_PREFIX'] = env['toolchainbin'] + '/' + tool_prefix 104 | else: 105 | env['TOOL_PREFIX'] = tool_prefix 106 | 107 | if env['platform'] == 'osx': 108 | for dylib in installed_dylib: 109 | env.OSXRename(None, dylib) 110 | 111 | env.Append(LIBPATH=[output_path]) 112 | env.Append(LIBS=['avformat']) 113 | env.Append(LIBS=['avcodec']) 114 | env.Append(LIBS=['avutil']) 115 | env.Append(LIBS=['swscale']) 116 | env.Append(LIBS=['swresample']) 117 | if msvc_build: 118 | env.Append(LIBS=['WinMM.lib']) 119 | 120 | 121 | sources = list(map(lambda x: '#'+x, glob('src/*.c'))) 122 | 123 | # msvc doesn't prefix dll files with 'lib' 124 | libprefix = 'lib' if msvc_build else '' 125 | if env['debug']: 126 | env['PDB'] = output_path+libprefix+'gdnative_videodecoder.pdb' 127 | output_dylib = env.SharedLibrary(output_path+libprefix+'gdnative_videodecoder',sources) 128 | 129 | if env['platform'] == 'osx': 130 | env.OSXRename(None, output_dylib) 131 | 132 | if env['prefix']: 133 | path = env['prefix'] + '/' + env['platform'] + '/' 134 | Default(env.Install(path, ffmpeg_dylibs + output_dylib)) 135 | elif env['test']: 136 | env.Install('#test/addons/' + output_path[1:], output_dylib + ffmpeg_dylibs) 137 | -------------------------------------------------------------------------------- /build_gdnative.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # For osx you must download XCode 7 and extract/generate ./darwin_sdk/MacOSX10.11.sdk.tar.gz 4 | # by following these instructions: https://github.com/tpoechtrager/osxcross#packaging-the-sdk 5 | # NOTE: for darwin15 support use: https://developer.apple.com/download/more/?name=Xcode%207.3.1 6 | # To use a different MacOSX*.*.sdk.tar.gz sdk set XCODE_SDK 7 | # e.g. XCODE_SDK=$PWD/darwin_sdk/MacOSX10.15.sdk.tar.gz ./build_gdnative.sh 8 | 9 | # usage: ADDON_BIN_DIR=$PWD/godot/addons/bin ./contrib/godot-videodecoder/build_gdnative.sh 10 | # (from within your project where this is a submodule installed at ./contrib/godot-videodecoder/build_gdnative.sh/) 11 | 12 | # The Dockerfile will run a container to compile everything: 13 | # http://docs.godotengine.org/en/3.2/development/compiling/compiling_for_x11.html 14 | # http://docs.godotengine.org/en/3.2/development/compiling/compiling_for_windows.html#cross-compiling-for-windows-from-other-operating-systems 15 | # http://docs.godotengine.org/en/3.2/development/compiling/compiling_for_osx.html#cross-compiling-for-macos-from-linux 16 | 17 | DIR="$(cd $(dirname "$0") && pwd)" 18 | ADDON_BIN_DIR=${ADDON_BIN_DIR:-$DIR/target} 19 | THIRDPARTY_DIR=${THIRDPARTY_DIR:-$DIR/thirdparty} 20 | # COPY can't use variables, so pre-copy the file 21 | XCODE_SDK_FOR_COPY=./darwin_sdk/MacOSX10.11.sdk.tar.xz 22 | XCODE_SDK="${XCODE_SDK:-$XCODE_SDK_FOR_COPY}" 23 | 24 | SUBMODULES_OK=1 25 | if ! [ -f "$DIR/godot_include/gdnative_api_struct.gen.h" ]; then 26 | echo "godot_include headers are missing." 27 | echo " Please run the following:" 28 | echo "" 29 | echo " git submodule update --init godot_include" 30 | echo "" 31 | SUBMODULES_OK= 32 | fi 33 | 34 | if ! [ -f "$DIR/ffmpeg-static/build.sh" ]; then 35 | echo "godot_include headers are missing." 36 | echo " Please run the following:" 37 | echo "" 38 | echo " git submodule update --init ffmpeg-static" 39 | echo "" 40 | SUBMODULES_OK= 41 | fi 42 | if ! [ $SUBMODULES_OK ]; then 43 | echo "Aborting due to missing submodules" 44 | exit 1 45 | fi 46 | 47 | if [ -z "$PLATFORMS" ]; then 48 | PLATFORM_LIST=(win64 osx x11 win32 x11_32) 49 | else 50 | IFS=',' read -r -a PLATFORM_LIST <<< "$PLATFORMS" 51 | fi 52 | declare -A PLATMAP 53 | for p in "${PLATFORM_LIST[@]}"; do 54 | PLATMAP[$p]=1 55 | done 56 | 57 | echo "Building for ${PLATFORM_LIST[@]}" 58 | 59 | plat_win64=${PLATMAP['win64']} 60 | plat_win32=${PLATMAP['win32']} 61 | plat_osx=${PLATMAP['osx']} 62 | plat_x11=${PLATMAP['x11']} 63 | plat_x11_32=${PLATMAP['x11_32']} 64 | plat_win_any=${PLATMAP['win64']}${PLATMAP['win32']} 65 | plat_x11_any=${PLATMAP['x11']}${PLATMAP['x11_32']} 66 | 67 | if ! [ "$JOBS" ]; then 68 | if [ -f /proc/cpuinfo ]; then 69 | JOBS=$(expr $(cat /proc/cpuinfo | grep processor | wc -l) - 1) 70 | elif type sysctl > /dev/null; then 71 | # osx logical cores 72 | JOBS=$(sysctl -n hw.ncpu) 73 | else 74 | JOBS=4 75 | echo "Unable to determine how many logical cores are available." 76 | fi 77 | fi 78 | echo "Using JOBS=$JOBS" 79 | 80 | #img_version="$(git describe 2>/dev/null || git rev-parse HEAD)" 81 | # TODO : pass in img_version like https://github.com/godotengine/build-containers/blob/master/Dockerfile.osx#L1 82 | 83 | if [ $plat_osx ]; then 84 | echo "XCODE_SDK=$XCODE_SDK" 85 | if [ ! -f "$XCODE_SDK" ]; then 86 | ls -l "$XCODE_SDK" 87 | echo "Unable to find $XCODE_SDK" 88 | exit 1 89 | fi 90 | if [ ! "$XCODE_SDK" = "$XCODE_SDK_FOR_COPY" ]; then 91 | mkdir -p $(dirname "$XCODE_SDK_FOR_COPY") 92 | cp "$XCODE_SDK" "$XCODE_SDK_FOR_COPY" 93 | fi 94 | fi 95 | 96 | set -e 97 | # ideally we'd run these at the same time but ... https://github.com/moby/moby/issues/2776 98 | if [ $plat_x11_any ]; then 99 | docker build ./ -f Dockerfile.ubuntu-xenial -t "godot-videodecoder-ubuntu-xenial" 100 | fi 101 | 102 | # bionic is for cross compiles, use xenial for linux 103 | # (for ubuntu 16 compatibility even though it's outdated already) 104 | if [ $plat_osx ]; then 105 | echo "building with xcode sdk" 106 | docker build ./ -f Dockerfile.ubuntu-bionic -t "godot-videodecoder-ubuntu-bionic" \ 107 | --build-arg XCODE_SDK=$XCODE_SDK 108 | elif [ $plat_win_any ]; then 109 | echo "building without xcode sdk" 110 | docker build ./ -f Dockerfile.ubuntu-bionic -t "godot-videodecoder-ubuntu-bionic" 111 | fi 112 | 113 | if [ $plat_osx ]; then 114 | echo "Building for OSX" 115 | docker build ./ -f Dockerfile.osx --build-arg JOBS=$JOBS -t "godot-videodecoder-osx" 116 | fi 117 | if [ $plat_x11 ]; then 118 | echo "Building for X11" 119 | docker build ./ -f Dockerfile.x11 --build-arg JOBS=$JOBS -t "godot-videodecoder-x11" 120 | fi 121 | if [ $plat_x11_32 ]; then 122 | docker build ./ -f Dockerfile.x11_32 --build-arg JOBS=$JOBS -t "godot-videodecoder-x11_32" 123 | fi 124 | if [ $plat_win64 ]; then 125 | echo "Building for Win64" 126 | docker build ./ -f Dockerfile.win64 --build-arg JOBS=$JOBS -t "godot-videodecoder-win64" 127 | fi 128 | if [ $plat_win32 ]; then 129 | docker build ./ -f Dockerfile.win32 --build-arg JOBS=$JOBS -t "godot-videodecoder-win32" 130 | fi 131 | 132 | set -x 133 | # precreate the target directory because otherwise 134 | # docker cp will copy x11/* -> $ADDON_BIN_DIR/* instead of x11/* -> $ADDON_BIN_DIR/x11/* 135 | mkdir -p $ADDON_BIN_DIR/ 136 | # copy the thirdparty dir in case you want to try building the lib against the ffmpeg libs directly e.g. in MSVC 137 | mkdir -p $THIRDPARTY_DIR 138 | 139 | if [ "$(uname -o)" = "Msys" ]; then 140 | export MSYS=winsymlinks:native 141 | fi 142 | 143 | # TODO: this should be a loop over all the platforms 144 | if [ $plat_x11 ]; then 145 | echo "extracting $ADDON_BIN_DIR/x11" 146 | id=$(docker create godot-videodecoder-x11) 147 | docker cp $id:/opt/target/x11 $ADDON_BIN_DIR/ 148 | mkdir -p $THIRDPARTY_DIR/x11 149 | 150 | # tar because copying a symlink on windows will fail if you don't run as administrator 151 | docker cp -L $id:/opt/godot-videodecoder/thirdparty/x11 - | tar -xhC $THIRDPARTY_DIR/ 152 | docker rm -v $id 153 | fi 154 | 155 | if [ $plat_x11 ]; then 156 | echo "extracting $ADDON_BIN_DIR/x11" 157 | id=$(docker create godot-videodecoder-x11) 158 | docker cp $id:/opt/target/x11 $ADDON_BIN_DIR/ 159 | 160 | mkdir -p $THIRDPARTY_DIR/x11 161 | # tar because copying a symlink on windows will fail if you don't run as administrator 162 | docker cp -L $id:/opt/godot-videodecoder/thirdparty/x11 - | tar -xhC $THIRDPARTY_DIR/ 163 | docker rm -v $id 164 | fi 165 | 166 | if [ $plat_x11_32 ]; then 167 | echo "extracting $ADDON_BIN_DIR/x11_32" 168 | id=$(docker create godot-videodecoder-x11_32) 169 | docker cp $id:/opt/target/x11_32 $ADDON_BIN_DIR/ 170 | 171 | mkdir -p $THIRDPARTY_DIR/x11_32 172 | # tar because copying a symlink on windows will fail if you don't run as administrator 173 | docker cp -L $id:/opt/godot-videodecoder/thirdparty/x11_32 - | tar -xhC $THIRDPARTY_DIR/ 174 | docker rm -v $id 175 | fi 176 | 177 | if [ $plat_osx ]; then 178 | echo "extracting $ADDON_BIN_DIR/osx" 179 | id=$(docker create godot-videodecoder-osx) 180 | docker cp $id:/opt/target/osx $ADDON_BIN_DIR/ 181 | 182 | mkdir -p $THIRDPARTY_DIR/osx 183 | # tar because copying a symlink on windows will fail if you don't run as administrator 184 | docker cp -L $id:/opt/godot-videodecoder/thirdparty/osx - | tar -xhC $THIRDPARTY_DIR/ 185 | docker rm -v $id 186 | fi 187 | 188 | if [ $plat_win64 ]; then 189 | echo "extracting $ADDON_BIN_DIR/win64" 190 | id=$(docker create godot-videodecoder-win64) 191 | docker cp $id:/opt/target/win64 $ADDON_BIN_DIR/ 192 | 193 | mkdir -p $THIRDPARTY_DIR/win64 194 | # tar because copying a symlink on windows will fail if you don't run as administrator 195 | docker cp -L $id:/opt/godot-videodecoder/thirdparty/win64 - | tar -xhC $THIRDPARTY_DIR/ 196 | docker rm -v $id 197 | fi 198 | 199 | if [ $plat_win32 ]; then 200 | echo "extracting $ADDON_BIN_DIR/win32" 201 | id=$(docker create godot-videodecoder-win32) 202 | docker cp $id:/opt/target/win32 $ADDON_BIN_DIR/ 203 | 204 | mkdir -p $THIRDPARTY_DIR/win32 205 | # tar because copying a symlink on windows will fail if you don't run as administrator 206 | docker cp -L $id:/opt/godot-videodecoder/thirdparty/win32 - | tar -xhC $THIRDPARTY_DIR/ 207 | docker rm -v $id 208 | fi 209 | 210 | if type tree 2> /dev/null; then 211 | tree $THIRDPARTY_DIR -L 2 -hD 212 | tree $ADDON_BIN_DIR -hD 213 | else 214 | find $THIRDPARTY_DIR -maxdepth 2 -print -exec ls -lh {} \; 215 | find $ADDON_BIN_DIR -maxdepth 1 -print -exec ls -lh {} \; 216 | fi -------------------------------------------------------------------------------- /build_gdnative.sh.example: -------------------------------------------------------------------------------- 1 | ADDON_BIN_DIR=$PWD/godot/addons/bin ./contrib/godot-videodecoder/build_gdnative.sh "$@" 2 | -------------------------------------------------------------------------------- /build_gdnative_test.sh: -------------------------------------------------------------------------------- 1 | ADDON_BIN_DIR=test/addons/bin ./build_gdnative.sh "$@" 2 | -------------------------------------------------------------------------------- /darwin_sdk/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kidrigger/godot-videodecoder/af71383dce66eea88f3ce99b416613b925be3ba0/darwin_sdk/.gitignore -------------------------------------------------------------------------------- /renamer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import subprocess 4 | 5 | def rename_files(prefix, changeto, tool_prefix, filenames): 6 | renamer_buffer = 'RENAMER_BUFFER_314159265358979' 7 | otool = tool_prefix + 'otool' 8 | install_name_tool = tool_prefix + 'install_name_tool' 9 | for filename in filenames: 10 | data = str(subprocess.check_output([otool,'-L',filename])).strip() 11 | val = map(lambda x: x[0], map(str.split,map(str.strip, data.strip().split('\n')))) 12 | val = list(val)[2:] 13 | 14 | to_change = {} 15 | for path in val: 16 | if path.startswith(prefix): 17 | to_change[path] = changeto+path[len(prefix):] 18 | 19 | for k,v in to_change.items(): 20 | print(k, v, sep=' -> ') 21 | subprocess.call([install_name_tool,'-change',k,v,filename]) 22 | 23 | 24 | if __name__ == '__main__': 25 | from sys import argv 26 | name, prefix, changeto, tool_prefix = argv[0:4] 27 | filenames = argv[4:] 28 | rename_files(prefix, changeto, tool_prefix, filenames) 29 | -------------------------------------------------------------------------------- /src/gdnative_videodecoder.c: -------------------------------------------------------------------------------- 1 | #ifdef _MSC_VER 2 | #include 3 | #else 4 | #include 5 | #endif 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include "packet_queue.h" 21 | #include "set.h" 22 | 23 | #ifdef __APPLE__ 24 | #include 25 | #include 26 | #endif 27 | 28 | // TODO: is this sample rate defined somewhere in the godot api etc? 29 | #define AUDIO_MIX_RATE 22050 30 | 31 | enum POSITION_TYPE {POS_V_PTS, POS_TIME, POS_A_TIME}; 32 | typedef struct videodecoder_data_struct { 33 | 34 | godot_object *instance; // Don't clean 35 | AVIOContext *io_ctx; 36 | AVFormatContext *format_ctx; 37 | AVCodecContext *vcodec_ctx; 38 | AVFrame *frame_yuv; 39 | AVFrame *frame_rgb; 40 | 41 | struct SwsContext *sws_ctx; 42 | uint8_t *frame_buffer; 43 | 44 | int videostream_idx; 45 | int frame_buffer_size; 46 | godot_pool_byte_array unwrapped_frame; 47 | godot_real time; 48 | 49 | double audio_time; 50 | double diff_tolerance; 51 | 52 | int audiostream_idx; 53 | AVCodecContext *acodec_ctx; 54 | godot_bool acodec_open; 55 | AVFrame *audio_frame; 56 | void *mix_udata; 57 | 58 | int num_decoded_samples; 59 | float *audio_buffer; 60 | int audio_buffer_pos; 61 | 62 | SwrContext *swr_ctx; 63 | 64 | PacketQueue *audio_packet_queue; 65 | PacketQueue *video_packet_queue; 66 | 67 | unsigned long drop_frame; 68 | unsigned long total_frame; 69 | 70 | double seek_time; 71 | 72 | enum POSITION_TYPE position_type; 73 | uint8_t *io_buffer; 74 | godot_bool vcodec_open; 75 | godot_bool input_open; 76 | bool frame_unwrapped; 77 | 78 | } videodecoder_data_struct; 79 | 80 | const godot_int IO_BUFFER_SIZE = 512 * 1024; // File reading buffer of 512 KiB 81 | const godot_int AUDIO_BUFFER_MAX_SIZE = 192000; 82 | 83 | const godot_gdnative_core_api_struct *api = NULL; 84 | const godot_gdnative_ext_nativescript_api_struct *nativescript_api = NULL; 85 | const godot_gdnative_ext_nativescript_1_1_api_struct *nativescript_api_1_1 = NULL; 86 | const godot_gdnative_ext_videodecoder_api_struct *videodecoder_api = NULL; 87 | 88 | extern const godot_videodecoder_interface_gdnative plugin_interface; 89 | 90 | static const char *plugin_name = "ffmpeg_videoplayer"; 91 | static int num_supported_ext = 0; 92 | static char **supported_ext = NULL; 93 | 94 | /// Clock Setup function (used by get_ticks_usec) 95 | static uint64_t _clock_start = 0; 96 | #if defined(__APPLE__) 97 | static double _clock_scale = 0; 98 | static void _setup_clock() { 99 | mach_timebase_info_data_t info; 100 | kern_return_t ret = mach_timebase_info(&info); 101 | _clock_scale = ((double)info.numer / (double)info.denom) / 1000.0; 102 | _clock_start = mach_absolute_time() * _clock_scale; 103 | } 104 | #elif defined(_MSC_VER) 105 | uint64_t ticks_per_second; 106 | uint64_t ticks_start; 107 | static uint64_t get_ticks_usec(); 108 | static void _setup_clock() { 109 | // We need to know how often the clock is updated 110 | if (!QueryPerformanceFrequency((LARGE_INTEGER *)&ticks_per_second)) 111 | ticks_per_second = 1000; 112 | ticks_start = 0; 113 | ticks_start = get_ticks_usec(); 114 | } 115 | #else 116 | #if defined(CLOCK_MONOTONIC_RAW) && !defined(JAVASCRIPT_ENABLED) // This is a better clock on Linux. 117 | #define GODOT_CLOCK CLOCK_MONOTONIC_RAW 118 | #else 119 | #define GODOT_CLOCK CLOCK_MONOTONIC 120 | #endif 121 | static void _setup_clock() { 122 | struct timespec tv_now = { 0, 0 }; 123 | clock_gettime(GODOT_CLOCK, &tv_now); 124 | _clock_start = ((uint64_t)tv_now.tv_nsec / 1000L) + (uint64_t)tv_now.tv_sec * 1000000L; 125 | } 126 | #endif 127 | static uint64_t get_ticks_usec() { 128 | #if defined(_MSC_VER) 129 | 130 | uint64_t ticks; 131 | 132 | // This is the number of clock ticks since start 133 | if (!QueryPerformanceCounter((LARGE_INTEGER *)&ticks)) 134 | ticks = (UINT64)timeGetTime(); 135 | 136 | // Divide by frequency to get the time in seconds 137 | // original calculation shown below is subject to overflow 138 | // with high ticks_per_second and a number of days since the last reboot. 139 | // time = ticks * 1000000L / ticks_per_second; 140 | 141 | // we can prevent this by either using 128 bit math 142 | // or separating into a calculation for seconds, and the fraction 143 | uint64_t seconds = ticks / ticks_per_second; 144 | 145 | // compiler will optimize these two into one divide 146 | uint64_t leftover = ticks % ticks_per_second; 147 | 148 | // remainder 149 | uint64_t time = (leftover * 1000000L) / ticks_per_second; 150 | 151 | // seconds 152 | time += seconds * 1000000L; 153 | 154 | // Subtract the time at game start to get 155 | // the time since the game started 156 | time -= ticks_start; 157 | return time; 158 | #else 159 | #if defined(__APPLE__) 160 | uint64_t longtime = mach_absolute_time() * _clock_scale; 161 | #else 162 | struct timespec tv_now = { 0, 0 }; 163 | clock_gettime(GODOT_CLOCK, &tv_now); 164 | uint64_t longtime = ((uint64_t)tv_now.tv_nsec / 1000L) + (uint64_t)tv_now.tv_sec * 1000000L; 165 | #endif 166 | longtime -= _clock_start; 167 | 168 | return longtime; 169 | #endif 170 | } 171 | 172 | static uint64_t get_ticks_msec() { 173 | return get_ticks_usec() / 1000L; 174 | } 175 | 176 | #define STRINGIFY(x) #x 177 | #define PROFILE_START(sig, line) const char __profile_sig__[] = "gdnative_videodecoder.c::" STRINGIFY(line) "::" sig; \ 178 | uint64_t __profile_ticks_start__ = get_ticks_usec() 179 | 180 | #define PROFILE_END if (nativescript_api_1_1) \ 181 | nativescript_api_1_1->godot_nativescript_profiling_add_data( \ 182 | __profile_sig__, get_ticks_usec() - __profile_ticks_start__ \ 183 | ) 184 | 185 | // Cleanup should empty the struct to the point where you can open a new file from. 186 | static void _cleanup(videodecoder_data_struct *data) { 187 | 188 | if (data->audio_packet_queue != NULL) { 189 | packet_queue_deinit(data->audio_packet_queue); 190 | data->audio_packet_queue = NULL; 191 | } 192 | 193 | if (data->video_packet_queue != NULL) { 194 | packet_queue_deinit(data->video_packet_queue); 195 | data->video_packet_queue = NULL; 196 | } 197 | 198 | if (data->sws_ctx != NULL) { 199 | sws_freeContext(data->sws_ctx); 200 | data->sws_ctx = NULL; 201 | } 202 | 203 | if (data->audio_frame != NULL) { 204 | av_frame_unref(data->audio_frame); 205 | data->audio_frame = NULL; 206 | } 207 | 208 | if (data->frame_rgb != NULL) { 209 | av_frame_unref(data->frame_rgb); 210 | data->frame_rgb = NULL; 211 | } 212 | 213 | if (data->frame_yuv != NULL) { 214 | av_frame_unref(data->frame_yuv); 215 | data->frame_yuv = NULL; 216 | } 217 | 218 | if (data->frame_buffer != NULL) { 219 | api->godot_free(data->frame_buffer); 220 | data->frame_buffer = NULL; 221 | data->frame_buffer_size = 0; 222 | } 223 | 224 | if (data->vcodec_ctx != NULL) { 225 | if (data->vcodec_open) { 226 | avcodec_close(data->vcodec_ctx); 227 | data->vcodec_open = GODOT_FALSE; 228 | } 229 | avcodec_free_context(&data->vcodec_ctx); 230 | data->vcodec_ctx = NULL; 231 | } 232 | 233 | if (data->acodec_ctx != NULL) { 234 | if (data->acodec_open) { 235 | avcodec_close(data->acodec_ctx); 236 | data->vcodec_open = GODOT_FALSE; 237 | avcodec_free_context(&data->acodec_ctx); 238 | data->acodec_ctx = NULL; 239 | } 240 | } 241 | 242 | if (data->format_ctx != NULL) { 243 | if (data->input_open) { 244 | avformat_close_input(&data->format_ctx); 245 | data->input_open = GODOT_FALSE; 246 | } 247 | avformat_free_context(data->format_ctx); 248 | data->format_ctx = NULL; 249 | } 250 | 251 | if (data->io_ctx != NULL) { 252 | avio_context_free(&data->io_ctx); 253 | data->io_ctx = NULL; 254 | } 255 | 256 | if (data->io_buffer != NULL) { 257 | api->godot_free(data->io_buffer); 258 | data->io_buffer = NULL; 259 | } 260 | 261 | if (data->audio_buffer != NULL) { 262 | api->godot_free(data->audio_buffer); 263 | data->audio_buffer = NULL; 264 | } 265 | 266 | if (data->swr_ctx != NULL) { 267 | swr_free(&data->swr_ctx); 268 | data->swr_ctx = NULL; 269 | } 270 | 271 | data->time = 0; 272 | data->seek_time = 0; 273 | data->diff_tolerance = 0; 274 | data->videostream_idx = -1; 275 | data->audiostream_idx = -1; 276 | data->num_decoded_samples = 0; 277 | data->audio_buffer_pos = 0; 278 | 279 | data->drop_frame = data->total_frame = 0; 280 | } 281 | 282 | static void _unwrap_video_frame(godot_pool_byte_array *dest, AVFrame *frame, int width, int height) { 283 | int frame_size = width * height * 4; 284 | if (api->godot_pool_byte_array_size(dest) != frame_size) { 285 | api->godot_pool_byte_array_resize(dest, frame_size); 286 | } 287 | 288 | godot_pool_byte_array_write_access *write_access = api->godot_pool_byte_array_write(dest); 289 | uint8_t *write_ptr = api->godot_pool_byte_array_write_access_ptr(write_access); 290 | int val = 0; 291 | for (int y = 0; y < height; y++) { 292 | memcpy(write_ptr, frame->data[0] + y * frame->linesize[0], width * 4); 293 | write_ptr += width * 4; 294 | } 295 | api->godot_pool_byte_array_write_access_destroy(write_access); 296 | } 297 | 298 | static int _interleave_audio_frame(float *dest, AVFrame *audio_frame) { 299 | float **audio_frame_data = (float **)audio_frame->data; 300 | int count = 0; 301 | for (int j = 0; j != audio_frame->nb_samples; j++) { 302 | for (int i = 0; i != audio_frame->channels; i++) { 303 | dest[count++] = audio_frame_data[i][j]; 304 | } 305 | } 306 | return audio_frame->nb_samples; 307 | } 308 | 309 | static void _update_extensions() { 310 | if (num_supported_ext > 0) return; 311 | 312 | const AVInputFormat *current_fmt = NULL; 313 | set_t *sup_ext_set = NULL; 314 | void *iterator_opaque = NULL; 315 | while ((current_fmt = av_demuxer_iterate(&iterator_opaque)) != NULL) { 316 | if (current_fmt->extensions != NULL) { 317 | char *exts = (char *)api->godot_alloc(strlen(current_fmt->extensions) + 1); 318 | strcpy(exts, current_fmt->extensions); 319 | char *token = strtok(exts, ","); 320 | while (token != NULL) { 321 | sup_ext_set = set_insert(sup_ext_set, token); 322 | token = strtok(NULL, ", "); 323 | } 324 | api->godot_free(exts); 325 | if (current_fmt->mime_type) { 326 | char *mime_types = (char *)api->godot_alloc(strlen(current_fmt->mime_type) + 1); 327 | strcpy(mime_types, current_fmt->mime_type); 328 | char *token = strtok(mime_types, ","); 329 | // for some reason the webm extension is missing from the format that supports it 330 | while (token != NULL) { 331 | if (strcmp("video/webm", token) == 0) { 332 | sup_ext_set = set_insert(sup_ext_set, "webm"); 333 | } 334 | token = strtok(NULL, ","); 335 | } 336 | api->godot_free(mime_types); 337 | } 338 | } 339 | } 340 | 341 | list_t ext_list = set_create_list(sup_ext_set); 342 | num_supported_ext = list_size(&ext_list); 343 | supported_ext = (char **)api->godot_alloc(sizeof(char *) * num_supported_ext); 344 | list_node_t *cur_node = ext_list.start; 345 | int i = 0; 346 | while (cur_node != NULL) { 347 | supported_ext[i] = cur_node->value; 348 | cur_node->value = NULL; 349 | cur_node = cur_node->next; 350 | i++; 351 | } 352 | list_free(&ext_list); 353 | set_free(sup_ext_set); 354 | } 355 | 356 | static inline godot_real _avtime_to_sec(int64_t avtime) { 357 | return avtime / (godot_real)AV_TIME_BASE; 358 | } 359 | 360 | static void _godot_print(char *msg) { 361 | godot_string g_msg = {0}; 362 | g_msg = api->godot_string_chars_to_utf8(msg); 363 | api->godot_print(&g_msg); 364 | api->godot_string_destroy(&g_msg); 365 | } 366 | 367 | static void print_codecs() { 368 | 369 | const AVCodecDescriptor *desc = NULL; 370 | unsigned nb_codecs = 0, i = 0; 371 | 372 | char msg[512] = {0}; 373 | snprintf(msg, sizeof(msg) - 1, "%s: Supported video codecs:", plugin_name); 374 | _godot_print(msg); 375 | while ((desc = avcodec_descriptor_next(desc))) { 376 | const AVCodec* codec = NULL; 377 | void* i = NULL; 378 | bool found = false; 379 | while ((codec = av_codec_iterate(&i))) { 380 | if (codec->id == desc->id && av_codec_is_decoder(codec)) { 381 | if (!found && avcodec_find_decoder(desc->id) || avcodec_find_encoder(desc->id)) { 382 | 383 | snprintf(msg, sizeof(msg) - 1, "\t%s%s%s", 384 | avcodec_find_decoder(desc->id) ? "decode " : "", 385 | avcodec_find_encoder(desc->id) ? "encode " : "", 386 | desc->name 387 | ); 388 | found = true; 389 | _godot_print(msg); 390 | } 391 | if (strcmp(codec->name, desc->name) != 0) { 392 | snprintf(msg, sizeof(msg) - 1, "\t codec: %s", codec->name); 393 | _godot_print(msg); 394 | } 395 | } 396 | } 397 | } 398 | } 399 | 400 | inline static bool api_ver(godot_gdnative_api_version v, unsigned int want_major, unsigned int want_minor) { 401 | return v.major == want_major && v.minor == want_minor; 402 | } 403 | 404 | 405 | void GDN_EXPORT godot_gdnative_init(godot_gdnative_init_options *p_options) { 406 | _setup_clock(); 407 | api = p_options->api_struct; 408 | for (int i = 0; i < api->num_extensions; i++) { 409 | switch (api->extensions[i]->type) { 410 | case GDNATIVE_EXT_VIDEODECODER: 411 | videodecoder_api = (godot_gdnative_ext_videodecoder_api_struct *)api->extensions[i]; 412 | 413 | break; 414 | case GDNATIVE_EXT_NATIVESCRIPT: 415 | nativescript_api = (godot_gdnative_ext_nativescript_api_struct *)api->extensions[i]; 416 | const godot_gdnative_api_struct *ext_next = nativescript_api->next; 417 | while (ext_next) { 418 | if (api_ver(ext_next->version, 1, 1)) { 419 | nativescript_api_1_1 = (godot_gdnative_ext_nativescript_1_1_api_struct *)ext_next; 420 | break; 421 | } 422 | ext_next = ext_next->next; 423 | } 424 | break; 425 | default: break; 426 | } 427 | } 428 | print_codecs(); 429 | } 430 | 431 | void GDN_EXPORT godot_gdnative_terminate(godot_gdnative_terminate_options *p_options) { 432 | api = NULL; 433 | } 434 | 435 | void GDN_EXPORT godot_gdnative_singleton() { 436 | if (videodecoder_api != NULL) { 437 | videodecoder_api->godot_videodecoder_register_decoder(&plugin_interface); 438 | } 439 | } 440 | 441 | void *godot_videodecoder_constructor(godot_object *p_instance) { 442 | videodecoder_data_struct *data = api->godot_alloc(sizeof(videodecoder_data_struct)); 443 | 444 | data->instance = p_instance; 445 | 446 | data->io_buffer = NULL; 447 | data->io_ctx = NULL; 448 | 449 | data->format_ctx = NULL; 450 | data->input_open = GODOT_FALSE; 451 | 452 | data->videostream_idx = -1; 453 | data->vcodec_ctx = NULL; 454 | data->vcodec_open = GODOT_FALSE; 455 | 456 | data->frame_rgb = NULL; 457 | data->frame_yuv = NULL; 458 | data->sws_ctx = NULL; 459 | 460 | data->frame_buffer = NULL; 461 | data->frame_buffer_size = 0; 462 | 463 | data->audiostream_idx = -1; 464 | data->acodec_ctx = NULL; 465 | data->acodec_open = GODOT_FALSE; 466 | data->audio_frame = NULL; 467 | data->audio_buffer = NULL; 468 | 469 | data->swr_ctx = NULL; 470 | 471 | data->num_decoded_samples = 0; 472 | data->audio_buffer_pos = 0; 473 | 474 | data->audio_packet_queue = NULL; 475 | data->video_packet_queue = NULL; 476 | 477 | data->position_type = POS_A_TIME; 478 | data->time = 0; 479 | data->audio_time = NAN; 480 | 481 | data->frame_unwrapped = false; 482 | api->godot_pool_byte_array_new(&data->unwrapped_frame); 483 | 484 | return data; 485 | } 486 | 487 | void godot_videodecoder_destructor(void *p_data) { 488 | videodecoder_data_struct *data = (videodecoder_data_struct *)p_data; 489 | _cleanup(data); 490 | 491 | data->instance = NULL; 492 | api->godot_pool_byte_array_destroy(&data->unwrapped_frame); 493 | 494 | api->godot_free(data); 495 | data = NULL; // Not needed, but just to be safe. 496 | 497 | if (num_supported_ext > 0) { 498 | for (int i = 0; i < num_supported_ext; i++) { 499 | if (supported_ext[i] != NULL) { 500 | api->godot_free((void *)supported_ext[i]); 501 | } 502 | } 503 | api->godot_free(supported_ext); 504 | num_supported_ext = 0; 505 | } 506 | } 507 | 508 | const char **godot_videodecoder_get_supported_ext(int *p_count) { 509 | _update_extensions(); 510 | *p_count = num_supported_ext; 511 | return (const char **)supported_ext; 512 | } 513 | 514 | const char *godot_videodecoder_get_plugin_name(void) { 515 | return plugin_name; 516 | } 517 | 518 | godot_bool godot_videodecoder_open_file(void *p_data, void *file) { 519 | videodecoder_data_struct *data = (videodecoder_data_struct *)p_data; 520 | 521 | // Clean up the previous file. 522 | _cleanup(data); 523 | 524 | data->io_buffer = (uint8_t *)api->godot_alloc(IO_BUFFER_SIZE * sizeof(uint8_t)); 525 | if (data->io_buffer == NULL) { 526 | _cleanup(data); 527 | api->godot_print_warning("Buffer alloc error", "godot_videodecoder_open_file()", __FILE__, __LINE__); 528 | return GODOT_FALSE; 529 | } 530 | 531 | godot_int read_bytes = videodecoder_api->godot_videodecoder_file_read(file, data->io_buffer, IO_BUFFER_SIZE); 532 | 533 | // Rewind to 0 534 | videodecoder_api->godot_videodecoder_file_seek(file, 0, SEEK_SET); 535 | 536 | // Determine input format 537 | AVProbeData probe_data; 538 | probe_data.buf = data->io_buffer; 539 | probe_data.buf_size = read_bytes; 540 | probe_data.filename = ""; 541 | probe_data.mime_type = ""; 542 | 543 | AVInputFormat *input_format = NULL; 544 | input_format = av_probe_input_format(&probe_data, 1); 545 | if (input_format == NULL) { 546 | _cleanup(data); 547 | char msg[512] = {0}; 548 | snprintf(msg, sizeof(msg) - 1, "Format not recognized: %s (%s)", probe_data.filename, probe_data.mime_type); 549 | api->godot_print_error(msg, "godot_videodecoder_open_file()", __FILE__, __LINE__); 550 | return GODOT_FALSE; 551 | } 552 | input_format->flags |= AVFMT_SEEK_TO_PTS; 553 | 554 | data->io_ctx = avio_alloc_context(data->io_buffer, IO_BUFFER_SIZE, 0, file, 555 | videodecoder_api->godot_videodecoder_file_read, NULL, 556 | videodecoder_api->godot_videodecoder_file_seek); 557 | if (data->io_ctx == NULL) { 558 | _cleanup(data); 559 | api->godot_print_error("IO context alloc error.", "godot_videodecoder_open_file()", __FILE__, __LINE__); 560 | return GODOT_FALSE; 561 | } 562 | 563 | data->format_ctx = avformat_alloc_context(); 564 | if (data->format_ctx == NULL) { 565 | _cleanup(data); 566 | api->godot_print_error("Format context alloc error.", "godot_videodecoder_open_file()", __FILE__, __LINE__); 567 | return GODOT_FALSE; 568 | } 569 | 570 | data->format_ctx->pb = data->io_ctx; 571 | data->format_ctx->flags = AVFMT_FLAG_CUSTOM_IO; 572 | data->format_ctx->iformat = input_format; 573 | 574 | if (avformat_open_input(&data->format_ctx, "", NULL, NULL) != 0) { 575 | _cleanup(data); 576 | api->godot_print_error("Input stream failed to open", "godot_videodecoder_open_file()", __FILE__, __LINE__); 577 | return GODOT_FALSE; 578 | } 579 | data->input_open = GODOT_TRUE; 580 | 581 | if (avformat_find_stream_info(data->format_ctx, NULL) < 0) { 582 | _cleanup(data); 583 | api->godot_print_error("Could not find stream info.", "godot_videodecoder_open_file()", __FILE__, __LINE__); 584 | return GODOT_FALSE; 585 | } 586 | 587 | data->videostream_idx = -1; // should be -1 anyway, just being paranoid. 588 | data->audiostream_idx = -1; 589 | // find stream 590 | for (int i = 0; i < data->format_ctx->nb_streams; i++) { 591 | if (data->format_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { 592 | data->videostream_idx = i; 593 | } else if (data->format_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { 594 | data->audiostream_idx = i; 595 | } 596 | } 597 | if (data->videostream_idx == -1) { 598 | _cleanup(data); 599 | api->godot_print_error("Video Stream not found.", "godot_videodecoder_open_file()", __FILE__, __LINE__); 600 | return GODOT_FALSE; 601 | } 602 | 603 | AVCodecParameters *vcodec_param = data->format_ctx->streams[data->videostream_idx]->codecpar; 604 | 605 | AVCodec *vcodec = NULL; 606 | vcodec = avcodec_find_decoder(vcodec_param->codec_id); 607 | if (vcodec == NULL) { 608 | const AVCodecDescriptor *desc = avcodec_descriptor_get(vcodec_param->codec_id); 609 | char msg[512] = {0}; 610 | snprintf(msg, sizeof(msg) - 1, "Videodecoder %s (%s) not found.", desc->name, desc->long_name); 611 | api->godot_print_warning(msg, "godot_videodecoder_open_file()", __FILE__, __LINE__); 612 | _cleanup(data); 613 | return GODOT_FALSE; 614 | } 615 | 616 | data->vcodec_ctx = avcodec_alloc_context3(vcodec); 617 | if (data->vcodec_ctx == NULL) { 618 | _cleanup(data); 619 | api->godot_print_warning("Videocodec allocation error.", "godot_videodecoder_open_file()", __FILE__, __LINE__); 620 | return GODOT_FALSE; 621 | } 622 | 623 | if (avcodec_parameters_to_context(data->vcodec_ctx, vcodec_param) < 0) { 624 | _cleanup(data); 625 | api->godot_print_warning("Videocodec context init error.", "godot_videodecoder_open_file()", __FILE__, __LINE__); 626 | return GODOT_FALSE; 627 | } 628 | // enable multi-thread decoding based on CPU core count 629 | data->vcodec_ctx->thread_count = 0; 630 | 631 | if (avcodec_open2(data->vcodec_ctx, vcodec, NULL) < 0) { 632 | _cleanup(data); 633 | api->godot_print_warning("Videocodec failed to open.", "godot_videodecoder_open_file()", __FILE__, __LINE__); 634 | return GODOT_FALSE; 635 | } 636 | data->vcodec_open = GODOT_TRUE; 637 | 638 | 639 | AVCodecParameters *acodec_param = NULL; 640 | AVCodec *acodec = NULL; 641 | if (data->audiostream_idx >= 0) { 642 | acodec_param = data->format_ctx->streams[data->audiostream_idx]->codecpar; 643 | 644 | acodec = avcodec_find_decoder(acodec_param->codec_id); 645 | if (acodec == NULL) { 646 | const AVCodecDescriptor *desc = avcodec_descriptor_get(acodec_param->codec_id); 647 | char msg[512] = {0}; 648 | snprintf(msg, sizeof(msg) - 1, "Audiodecoder %s (%s) not found.", desc-> name, desc->long_name); 649 | api->godot_print_warning(msg, "godot_videodecoder_open_file()", __FILE__, __LINE__); 650 | _cleanup(data); 651 | return GODOT_FALSE; 652 | } 653 | data->acodec_ctx = avcodec_alloc_context3(acodec); 654 | if (data->acodec_ctx == NULL) { 655 | _cleanup(data); 656 | api->godot_print_error("Audiocodec allocation error.", "godot_videodecoder_open_file()", __FILE__, __LINE__); 657 | return GODOT_FALSE; 658 | } 659 | 660 | if (avcodec_parameters_to_context(data->acodec_ctx, acodec_param) < 0) { 661 | _cleanup(data); 662 | api->godot_print_error("Audiocodec context init error.", "godot_videodecoder_open_file()", __FILE__, __LINE__); 663 | return GODOT_FALSE; 664 | } 665 | 666 | if (avcodec_open2(data->acodec_ctx, acodec, NULL) < 0) { 667 | _cleanup(data); 668 | api->godot_print_error("Audiocodec failed to open.", "godot_videodecoder_open_file()", __FILE__, __LINE__); 669 | return GODOT_FALSE; 670 | } 671 | data->acodec_open = GODOT_TRUE; 672 | 673 | data->audio_buffer = (float *)api->godot_alloc(AUDIO_BUFFER_MAX_SIZE * sizeof(float)); 674 | if (data->audio_buffer == NULL) { 675 | _cleanup(data); 676 | api->godot_print_error("Audio buffer alloc failed.", "godot_videodecoder_open_file()", __FILE__, __LINE__); 677 | return GODOT_FALSE; 678 | } 679 | 680 | data->audio_frame = av_frame_alloc(); 681 | if (data->audio_frame == NULL) { 682 | _cleanup(data); 683 | api->godot_print_error("Frame alloc fail.", "godot_videodecoder_open_file()", __FILE__, __LINE__); 684 | return GODOT_FALSE; 685 | } 686 | 687 | data->swr_ctx = swr_alloc(); 688 | av_opt_set_int(data->swr_ctx, "in_channel_layout", data->acodec_ctx->channel_layout, 0); 689 | av_opt_set_int(data->swr_ctx, "out_channel_layout", data->acodec_ctx->channel_layout, 0); 690 | av_opt_set_int(data->swr_ctx, "in_sample_rate", data->acodec_ctx->sample_rate, 0); 691 | av_opt_set_int(data->swr_ctx, "out_sample_rate", AUDIO_MIX_RATE, 0); 692 | av_opt_set_sample_fmt(data->swr_ctx, "in_sample_fmt", AV_SAMPLE_FMT_FLTP, 0); 693 | av_opt_set_sample_fmt(data->swr_ctx, "out_sample_fmt", AV_SAMPLE_FMT_FLT, 0); 694 | swr_init(data->swr_ctx); 695 | } 696 | 697 | // NOTE: Align of 1 (I think it is for 32 bit alignment.) Doesn't work otherwise 698 | data->frame_buffer_size = av_image_get_buffer_size(AV_PIX_FMT_RGB32, 699 | data->vcodec_ctx->width, data->vcodec_ctx->height, 1); 700 | 701 | data->frame_buffer = (uint8_t *)api->godot_alloc(data->frame_buffer_size); 702 | if (data->frame_buffer == NULL) { 703 | _cleanup(data); 704 | api->godot_print_error("Framebuffer alloc fail.", "godot_videodecoder_open_file()", __FILE__, __LINE__); 705 | return GODOT_FALSE; 706 | } 707 | 708 | data->frame_rgb = av_frame_alloc(); 709 | if (data->frame_rgb == NULL) { 710 | _cleanup(data); 711 | api->godot_print_error("Frame alloc fail.", "godot_videodecoder_open_file()", __FILE__, __LINE__); 712 | return GODOT_FALSE; 713 | } 714 | 715 | data->frame_yuv = av_frame_alloc(); 716 | if (data->frame_yuv == NULL) { 717 | _cleanup(data); 718 | api->godot_print_error("Frame alloc fail.", "godot_videodecoder_open_file()", __FILE__, __LINE__); 719 | return GODOT_FALSE; 720 | } 721 | 722 | int width = data->vcodec_ctx->width; 723 | int height = data->vcodec_ctx->height; 724 | if (av_image_fill_arrays(data->frame_rgb->data, data->frame_rgb->linesize, data->frame_buffer, 725 | AV_PIX_FMT_RGB32, width, height, 1) < 0) { 726 | _cleanup(data); 727 | api->godot_print_error("Frame fill.", "godot_videodecoder_open_file()", __FILE__, __LINE__); 728 | return GODOT_FALSE; 729 | } 730 | 731 | data->sws_ctx = sws_getContext(width, height, data->vcodec_ctx->pix_fmt, 732 | width, height, AV_PIX_FMT_RGB0, SWS_BILINEAR, 733 | NULL, NULL, NULL); 734 | if (data->sws_ctx == NULL) { 735 | _cleanup(data); 736 | api->godot_print_error("Swscale context not created.", "godot_videodecoder_open_file()", __FILE__, __LINE__); 737 | return GODOT_FALSE; 738 | } 739 | 740 | data->time = 0; 741 | data->num_decoded_samples = 0; 742 | 743 | data->audio_packet_queue = packet_queue_init(); 744 | data->video_packet_queue = packet_queue_init(); 745 | 746 | data->drop_frame = data->total_frame = 0; 747 | 748 | return GODOT_TRUE; 749 | } 750 | 751 | godot_real godot_videodecoder_get_length(const void *p_data) { 752 | videodecoder_data_struct *data = (videodecoder_data_struct *)p_data; 753 | 754 | if (data->format_ctx == NULL) { 755 | api->godot_print_warning("Format context is null.", "godot_videodecoder_get_length()", __FILE__, __LINE__); 756 | return -1; 757 | } 758 | 759 | return data->format_ctx->streams[data->videostream_idx]->duration * av_q2d(data->format_ctx->streams[data->videostream_idx]->time_base); 760 | } 761 | 762 | static bool read_frame(videodecoder_data_struct *data) { 763 | while (data->video_packet_queue->nb_packets < 24) { 764 | AVPacket pkt; 765 | int ret = av_read_frame(data->format_ctx, &pkt); 766 | if (ret >= 0) { 767 | if (pkt.stream_index == data->videostream_idx) { 768 | packet_queue_put(data->video_packet_queue, &pkt); 769 | } else if (pkt.stream_index == data->audiostream_idx) { 770 | packet_queue_put(data->audio_packet_queue, &pkt); 771 | } else { 772 | av_packet_unref(&pkt); 773 | } 774 | } else { 775 | return false; 776 | } 777 | } 778 | return true; 779 | } 780 | 781 | void godot_videodecoder_update(void *p_data, godot_real p_delta) { 782 | PROFILE_START("update", __LINE__); 783 | videodecoder_data_struct *data = (videodecoder_data_struct *)p_data; 784 | // during an 'update' make sure to use the video frame's pts timestamp 785 | // otherwise the godot VideoStreamNative update method 786 | // won't even try to request a frame since it expects the plugin 787 | // to use video presentation timestamp as the source of time. 788 | 789 | data->position_type = POS_V_PTS; 790 | 791 | data->time += p_delta; 792 | // afford one frame worth of slop when decoding 793 | data->diff_tolerance = p_delta; 794 | 795 | if (!isnan(data->audio_time)) { 796 | data->audio_time += p_delta; 797 | } 798 | read_frame(data); 799 | PROFILE_END; 800 | } 801 | 802 | godot_pool_byte_array *godot_videodecoder_get_videoframe(void *p_data) { 803 | PROFILE_START("get_videoframe", __LINE__); 804 | videodecoder_data_struct *data = (videodecoder_data_struct *)p_data; 805 | AVPacket pkt = {0}; 806 | int ret; 807 | size_t drop_count = 0; 808 | // to maintain a decent game frame rate 809 | // don't let frame decoding take more than this number of ms 810 | uint64_t max_frame_drop_time = 5; 811 | // but we do need to drop frames, so try to drop at least some frames even if it's a bit slow :( 812 | size_t min_frame_drop_count = 5; 813 | uint64_t start = get_ticks_msec(); 814 | 815 | retry: 816 | ret = avcodec_receive_frame(data->vcodec_ctx, data->frame_yuv); 817 | if (ret == AVERROR(EAGAIN)) { 818 | // need to call avcodedc_send_packet, get a packet from queue to send it 819 | while (!packet_queue_get(data->video_packet_queue, &pkt)) { 820 | //api->godot_print_warning("video packet queue empty", "godot_videodecoder_get_videoframe()", __FILE__, __LINE__); 821 | if (!read_frame(data)) { 822 | PROFILE_END; 823 | return NULL; 824 | } 825 | } 826 | ret = avcodec_send_packet(data->vcodec_ctx, &pkt); 827 | if (ret < 0) { 828 | char err[512]; 829 | char msg[768]; 830 | av_strerror(ret, err, sizeof(err) - 1); 831 | snprintf(msg, sizeof(msg) - 1, "avcodec_send_packet returns %d (%s)", ret, err); 832 | api->godot_print_error(msg, "godot_videodecoder_get_videoframe()", __FILE__, __LINE__); 833 | av_packet_unref(&pkt); 834 | PROFILE_END; 835 | return NULL; 836 | } 837 | av_packet_unref(&pkt); 838 | goto retry; 839 | } else if (ret < 0) { 840 | char msg[512] = {0}; 841 | snprintf(msg, sizeof(msg) - 1, "avcodec_receive_frame returns %d", ret); 842 | api->godot_print_error(msg, "godot_videodecoder_get_videoframe()", __FILE__, __LINE__); 843 | PROFILE_END; 844 | return NULL; 845 | } 846 | 847 | bool pts_correct = data->frame_yuv->pts == AV_NOPTS_VALUE; 848 | int64_t pts = pts_correct ? data->frame_yuv->pkt_dts : data->frame_yuv->pts; 849 | 850 | double ts = pts * av_q2d(data->format_ctx->streams[data->videostream_idx]->time_base); 851 | 852 | data->total_frame++; 853 | 854 | // frame successfully decoded here, now if it lags behind too much (diff_tolerance sec) 855 | // let's discard this frame and get the next frame instead 856 | bool drop = ts < data->time - data->diff_tolerance; 857 | uint64_t drop_duration = get_ticks_msec() - start; 858 | if (drop && drop_duration > max_frame_drop_time && drop_count < min_frame_drop_count && data->frame_unwrapped) { 859 | // only discard frames for max_frame_drop_time ms or we'll slow down the game's main thread! 860 | if (fabs(data->seek_time - data->time) > data->diff_tolerance * 10) { 861 | char msg[512]; 862 | snprintf(msg, sizeof(msg) -1, "Slow CPU? Dropped %d frames for %"PRId64"ms frame dropped: %lu/%lu (%.1f%%) pts=%.1f t=%.1f", 863 | (int)drop_count, 864 | drop_duration, 865 | data->drop_frame, 866 | data->total_frame, 867 | 100.0 * data->drop_frame / data->total_frame, 868 | ts, (double)data->time); 869 | api->godot_print_warning(msg, "godot_videodecoder_get_videoframe()", __FILE__, __LINE__); 870 | } 871 | } else if (drop) { 872 | drop_count++; 873 | data->drop_frame++; 874 | av_packet_unref(&pkt); 875 | goto retry; 876 | } 877 | if (!drop || fabs(data->seek_time - data->time) > data->diff_tolerance * 2) { 878 | // Don't overwrite the current frame when dropping frames for performance reasons 879 | // except when the time is within 2 frames of the most recent seek 880 | // because we don't want a glitchy 'fast forward' effect when seeking. 881 | // NOTE: VideoPlayer currently doesnt' ask for a frame when seeking while paused so you'd 882 | // have to fake it inside godot by unpausing briefly. (see FIG1 below) 883 | data->frame_unwrapped = true; 884 | sws_scale(data->sws_ctx, (uint8_t const *const *)data->frame_yuv->data, data->frame_yuv->linesize, 0, 885 | data->vcodec_ctx->height, data->frame_rgb->data, data->frame_rgb->linesize); 886 | _unwrap_video_frame(&data->unwrapped_frame, data->frame_rgb, data->vcodec_ctx->width, data->vcodec_ctx->height); 887 | } 888 | av_packet_unref(&pkt); 889 | 890 | // hack to get video_stream_gdnative to stop asking for frames. 891 | // stop trusting video pts until the next time update() is called. 892 | // this will unblock VideoStreamPlaybackGDNative::update() which 893 | // keeps calling get_texture() until the time matches 894 | // we don't need this behavior as we already handle frame skipping internally. 895 | data->position_type = POS_TIME; 896 | PROFILE_END; 897 | return data->frame_unwrapped ? &data->unwrapped_frame : NULL; 898 | } 899 | 900 | /* 901 | FIG1: how to seek while paused... 902 | 903 | var _paused_seeking = 0 904 | func seek_player(value): 905 | var was_playing = _playing 906 | if _playing: 907 | stop() 908 | _player.stream_position = value 909 | 910 | if was_playing: 911 | play(value) 912 | if _player.paused || _paused_seeking > 0: 913 | _player.paused = false 914 | _paused_seeking = _paused_seeking + 1 915 | # yes, it seems like 5 idle frames _is_ the magic number. 916 | # VideoPlayer gets notified to do NOTIFICATION_INTERNAL_PROCESS on idle frames 917 | # so this should always work? 918 | for i in range(5): 919 | yield(get_tree(), 'idle_frame') 920 | # WARNING: -= double decrements here somehow? 921 | _paused_seeking = _paused_seeking - 1 922 | assert(_paused_seeking >= 0) 923 | if _paused_seeking == 0: 924 | _player.paused = true 925 | 926 | */ 927 | 928 | godot_int godot_videodecoder_get_audio(void *p_data, float *pcm, int pcm_remaining) { 929 | PROFILE_START("get_audio", __LINE__); 930 | videodecoder_data_struct *data = (videodecoder_data_struct *)p_data; 931 | if (data->audiostream_idx < 0) { 932 | PROFILE_END; 933 | return 0; 934 | } 935 | bool first_frame = true; 936 | 937 | // if playback has just started or just seeked then we enter the audio_reset state. 938 | // during audio_reset it's important to skip old samples 939 | // _and_ avoid sending samples from the future until the presentation timestamp syncs up. 940 | bool audio_reset = isnan(data->audio_time) || data->audio_time > data->time - data->diff_tolerance; 941 | 942 | const int pcm_buffer_size = pcm_remaining; 943 | int pcm_offset = 0; 944 | 945 | double p_time = data->audio_frame->pts * av_q2d(data->format_ctx->streams[data->audiostream_idx]->time_base); 946 | 947 | if (audio_reset && data->num_decoded_samples > 0) { 948 | // don't send any pcm data if the frame hasn't started yet 949 | if (p_time > data->time) { 950 | PROFILE_END; 951 | return 0; 952 | } 953 | // skip the any decoded samples if their presentation timestamp is too old 954 | if (data->time - p_time > data->diff_tolerance) { 955 | data->num_decoded_samples = 0; 956 | } 957 | } 958 | 959 | int sample_count = (pcm_remaining < data->num_decoded_samples) ? pcm_remaining : data->num_decoded_samples; 960 | 961 | if (sample_count > 0) { 962 | memcpy(pcm, data->audio_buffer + data->acodec_ctx->channels * data->audio_buffer_pos, sizeof(float) * sample_count * data->acodec_ctx->channels); 963 | pcm_offset += sample_count; 964 | pcm_remaining -= sample_count; 965 | data->num_decoded_samples -= sample_count; 966 | data->audio_buffer_pos += sample_count; 967 | } 968 | while (pcm_remaining > 0) { 969 | if (data->num_decoded_samples <= 0) { 970 | AVPacket pkt; 971 | 972 | int ret; 973 | retry_audio: 974 | ret = avcodec_receive_frame(data->acodec_ctx, data->audio_frame); 975 | if (ret == AVERROR(EAGAIN)) { 976 | // need to call avcodec_send_packet, get a packet from queue to send it 977 | if (!packet_queue_get(data->audio_packet_queue, &pkt)) { 978 | if (pcm_offset == 0) { 979 | // if we haven't got any on-time audio yet, then the audio_time counter is meaningless. 980 | data->audio_time = NAN; 981 | } 982 | PROFILE_END; 983 | return pcm_offset; 984 | } 985 | ret = avcodec_send_packet(data->acodec_ctx, &pkt); 986 | if (ret < 0) { 987 | char msg[512]; 988 | snprintf(msg, sizeof(msg) -1, "avcodec_send_packet returns %d", ret); 989 | api->godot_print_error(msg, "godot_videodecoder_get_audio()", __FILE__, __LINE__); 990 | av_packet_unref(&pkt); 991 | PROFILE_END; 992 | return pcm_offset; 993 | } 994 | av_packet_unref(&pkt); 995 | goto retry_audio; 996 | } else if (ret < 0) { 997 | char msg[512]; 998 | snprintf(msg, sizeof(msg) - 1, "avcodec_receive_frame returns %d", ret); 999 | api->godot_print_error(msg, "godot_videodecoder_get_audio()", __FILE__, __LINE__); 1000 | PROFILE_END; 1001 | return pcm_buffer_size - pcm_remaining; 1002 | } 1003 | // only set the audio frame time if this is the first frame we've decoded during this update. 1004 | // any remaining frames are going into a buffer anyways 1005 | p_time = data->audio_frame->pts * av_q2d(data->format_ctx->streams[data->audiostream_idx]->time_base); 1006 | if (first_frame) { 1007 | data->audio_time = p_time; 1008 | first_frame = false; 1009 | } 1010 | // decoded audio ready here 1011 | data->num_decoded_samples = swr_convert(data->swr_ctx, (uint8_t **)&data->audio_buffer, data->audio_frame->nb_samples, (const uint8_t **)data->audio_frame->extended_data, data->audio_frame->nb_samples); 1012 | // data->num_decoded_samples = _interleave_audio_frame(data->audio_buffer, data->audio_frame); 1013 | data->audio_buffer_pos = 0; 1014 | } 1015 | if (audio_reset) { 1016 | if (data->time - p_time > data->diff_tolerance) { 1017 | // skip samples if the frame time is too far in the past 1018 | data->num_decoded_samples = 0; 1019 | } else if (p_time > data->time) { 1020 | // don't send any pcm data if the first frame hasn't started yet 1021 | data->audio_time = NAN; 1022 | break; 1023 | } 1024 | } 1025 | sample_count = pcm_remaining < data->num_decoded_samples ? pcm_remaining : data->num_decoded_samples; 1026 | if (sample_count > 0) { 1027 | memcpy(pcm + pcm_offset * data->acodec_ctx->channels, data->audio_buffer + data->acodec_ctx->channels * data->audio_buffer_pos, sizeof(float) * sample_count * data->acodec_ctx->channels); 1028 | pcm_offset += sample_count; 1029 | pcm_remaining -= sample_count; 1030 | data->num_decoded_samples -= sample_count; 1031 | data->audio_buffer_pos += sample_count; 1032 | } 1033 | } 1034 | 1035 | PROFILE_END; 1036 | return pcm_offset; 1037 | } 1038 | 1039 | godot_real godot_videodecoder_get_playback_position(const void *p_data) { 1040 | videodecoder_data_struct *data = (videodecoder_data_struct *)p_data; 1041 | 1042 | if (data->format_ctx) { 1043 | bool use_v_pts = data->frame_yuv->pts != AV_NOPTS_VALUE && data->position_type == POS_V_PTS; 1044 | bool use_a_time = data->position_type == POS_A_TIME; 1045 | bool in_update = data->position_type == POS_V_PTS; 1046 | data->position_type = POS_TIME; 1047 | 1048 | if (use_v_pts) { 1049 | double pts = (double)data->frame_yuv->pts; 1050 | pts *= av_q2d(data->format_ctx->streams[data->videostream_idx]->time_base); 1051 | return (godot_real)pts; 1052 | } else { 1053 | if (!isnan(data->audio_time) && use_a_time) { 1054 | return (godot_real)data->audio_time; 1055 | } 1056 | // fudge the time if we in the first frame after an update but don't have V_PTS yet 1057 | godot_real adjustment = in_update ? -0.01 : 0.0; 1058 | return (godot_real)data->time + adjustment; 1059 | } 1060 | } 1061 | return (godot_real)0; 1062 | } 1063 | 1064 | static void flush_frames(AVCodecContext* ctx) { 1065 | PROFILE_START("flush_frames", __LINE__); 1066 | /** 1067 | * from https://www.ffmpeg.org/doxygen/4.1/group__lavc__encdec.html 1068 | * End of stream situations. These require "flushing" (aka draining) the codec, as the codec might buffer multiple frames or packets internally for performance or out of necessity (consider B-frames). This is handled as follows: 1069 | * Instead of valid input, send NULL to the avcodec_send_packet() (decoding) or avcodec_send_frame() (encoding) functions. This will enter draining mode. 1070 | * Call avcodec_receive_frame() (decoding) or avcodec_receive_packet() (encoding) in a loop until AVERROR_EOF is returned. The functions will not return AVERROR(EAGAIN), unless you forgot to enter draining mode. 1071 | * Before decoding can be resumed again, the codec has to be reset with avcodec_flush_buffers(). 1072 | */ 1073 | int ret = avcodec_send_packet(ctx, NULL); 1074 | AVFrame frame = {0}; 1075 | if (ret <= 0) { 1076 | do { 1077 | ret = avcodec_receive_frame(ctx, &frame); 1078 | } while (ret != AVERROR_EOF); 1079 | } 1080 | PROFILE_END; 1081 | } 1082 | 1083 | void godot_videodecoder_seek(void *p_data, godot_real p_time) { 1084 | PROFILE_START("seek", __LINE__); 1085 | videodecoder_data_struct *data = (videodecoder_data_struct *)p_data; 1086 | // Hack to find the end of the video. Really VideoPlayer should expose this! 1087 | if (p_time < 0) { 1088 | p_time = _avtime_to_sec(data->format_ctx->duration); 1089 | } 1090 | int64_t seek_target = p_time * AV_TIME_BASE; 1091 | // seek within 10 seconds of the selected spot. 1092 | int64_t margin = 10 * AV_TIME_BASE; 1093 | 1094 | // printf("seek(): %fs = %lld\n", p_time, seek_target); 1095 | int ret = avformat_seek_file(data->format_ctx, -1, seek_target - margin, seek_target, seek_target, 0); 1096 | if (ret < 0) { 1097 | api->godot_print_warning("avformat_seek_file() can't seek backward?", "godot_videodecoder_seek()\n", __FILE__, __LINE__); 1098 | ret = avformat_seek_file(data->format_ctx, -1, seek_target - margin, seek_target, seek_target + margin, 0); 1099 | } 1100 | if (ret < 0) { 1101 | api->godot_print_error("avformat_seek_file() failed", "godot_videodecoder_seek()\n", __FILE__, __LINE__); 1102 | } else { 1103 | packet_queue_flush(data->video_packet_queue); 1104 | packet_queue_flush(data->audio_packet_queue); 1105 | flush_frames(data->vcodec_ctx); 1106 | avcodec_flush_buffers(data->vcodec_ctx); 1107 | if (data->acodec_ctx) { 1108 | flush_frames(data->acodec_ctx); 1109 | avcodec_flush_buffers(data->acodec_ctx); 1110 | } 1111 | data->num_decoded_samples = 0; 1112 | data->audio_buffer_pos = 0; 1113 | data->time = p_time; 1114 | data->seek_time = p_time; 1115 | // try to use the audio time as the seek position 1116 | data->position_type = POS_A_TIME; 1117 | data->audio_time = NAN; 1118 | } 1119 | PROFILE_END; 1120 | } 1121 | 1122 | /* ---------------------- TODO ------------------------- */ 1123 | 1124 | void godot_videodecoder_set_audio_track(void *p_data, godot_int p_audiotrack) { 1125 | // api->godot_print_warning("set_audio_track not implemented", "set_audio_track()\n", __FILE__, __LINE__); 1126 | } 1127 | 1128 | godot_int godot_videodecoder_get_channels(const void *p_data) { 1129 | videodecoder_data_struct *data = (videodecoder_data_struct *)p_data; 1130 | 1131 | if (data->acodec_ctx != NULL) { 1132 | return data->acodec_ctx->channels; 1133 | } 1134 | return 0; 1135 | } 1136 | 1137 | godot_int godot_videodecoder_get_mix_rate(const void *p_data) { 1138 | videodecoder_data_struct *data = (videodecoder_data_struct *)p_data; 1139 | 1140 | if (data->acodec_ctx != NULL) { 1141 | return AUDIO_MIX_RATE; 1142 | } 1143 | return 0; 1144 | } 1145 | 1146 | godot_vector2 godot_videodecoder_get_texture_size(const void *p_data) { 1147 | videodecoder_data_struct *data = (videodecoder_data_struct *)p_data; 1148 | godot_vector2 vec; 1149 | 1150 | if (data->vcodec_ctx != NULL) { 1151 | api->godot_vector2_new(&vec, data->vcodec_ctx->width, data->vcodec_ctx->height); 1152 | } 1153 | return vec; 1154 | } 1155 | 1156 | const godot_videodecoder_interface_gdnative plugin_interface = { 1157 | GODOTAV_API_MAJOR, GODOTAV_API_MINOR, 1158 | NULL, 1159 | godot_videodecoder_constructor, 1160 | godot_videodecoder_destructor, 1161 | godot_videodecoder_get_plugin_name, 1162 | godot_videodecoder_get_supported_ext, 1163 | godot_videodecoder_open_file, 1164 | godot_videodecoder_get_length, 1165 | godot_videodecoder_get_playback_position, 1166 | godot_videodecoder_seek, 1167 | godot_videodecoder_set_audio_track, 1168 | godot_videodecoder_update, 1169 | godot_videodecoder_get_videoframe, 1170 | godot_videodecoder_get_audio, 1171 | godot_videodecoder_get_channels, 1172 | godot_videodecoder_get_mix_rate, 1173 | godot_videodecoder_get_texture_size 1174 | }; 1175 | -------------------------------------------------------------------------------- /src/gdnative_videodecoder.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This won't compile yet. 3 | */ 4 | 5 | #ifndef FFMPEG_GDNATIVE_VIDEODECODER_H 6 | #define FFMPEG_GDNATIVE_VIDEODECODER_H 7 | 8 | #endif /* FFMPEG_GDNATIVE_VIDEODECODER_H */ -------------------------------------------------------------------------------- /src/linked_list.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef _LINKED_LIST_H 3 | #define _LINKED_LIST_H 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | extern const godot_gdnative_core_api_struct *api; 11 | 12 | typedef struct linked_list_node_t { 13 | char *value; 14 | struct linked_list_node_t *next; 15 | } list_node_t; 16 | 17 | typedef struct linked_list_t { 18 | list_node_t *start; 19 | list_node_t *end; 20 | } list_t; 21 | 22 | list_node_t *list_create_node(const char *str) { 23 | list_node_t *node = (list_node_t *)api->godot_alloc(sizeof(list_node_t)); 24 | node->value = (char *)api->godot_alloc(strlen(str) + 1); 25 | strcpy(node->value, str); 26 | node->next = NULL; 27 | return node; 28 | } 29 | 30 | void list_append(list_t *list, const char *str) { 31 | if (list->end == NULL) { 32 | list->start = list_create_node(str); 33 | list->end = list->start; 34 | return; 35 | } 36 | list->end->next = list_create_node(str); 37 | list->end = list->end->next; 38 | } 39 | 40 | void list_join(list_t *first, list_t *second) { 41 | if (second->start == NULL) return; 42 | first->end->next = second->start; 43 | first->end = second->end; 44 | second->start = NULL; 45 | second->end = NULL; 46 | } 47 | 48 | void list_free_r(list_node_t *head) { 49 | if (head == NULL) { 50 | return; 51 | } 52 | list_free_r(head->next); 53 | if (head->value != NULL) { 54 | api->godot_free(head->value); 55 | } 56 | api->godot_free(head); 57 | } 58 | 59 | void list_free(list_t *head) { 60 | list_free_r(head->start); 61 | head->end = NULL; 62 | head->start = NULL; 63 | } 64 | 65 | int list_size(list_t *head) { 66 | list_node_t *l = head->start; 67 | int i = 0; 68 | while (l != NULL) { 69 | i++; 70 | l = l->next; 71 | } 72 | return i; 73 | } 74 | 75 | #endif /* _LINKED_LIST_H */ 76 | -------------------------------------------------------------------------------- /src/packet_queue.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef _PACKET_QUEUE_H 3 | #define _PACKET_QUEUE_H 4 | 5 | #include 6 | #include 7 | 8 | extern const godot_gdnative_core_api_struct *api; 9 | 10 | typedef struct PacketQueue { 11 | AVPacketList *first_pkt, *last_pkt; 12 | int nb_packets; 13 | int size; 14 | } PacketQueue; 15 | 16 | int quit = 0; 17 | 18 | PacketQueue *packet_queue_init() { 19 | PacketQueue *q; 20 | q = (PacketQueue *)api->godot_alloc(sizeof(PacketQueue)); 21 | if (q != NULL) { 22 | memset(q, 0, sizeof(PacketQueue)); 23 | } 24 | return q; 25 | } 26 | 27 | void packet_queue_flush(PacketQueue *q) { 28 | AVPacketList *pkt, *pkt1; 29 | 30 | for (pkt = q->first_pkt; pkt; pkt = pkt1) { 31 | pkt1 = pkt->next; 32 | av_packet_unref(&pkt->pkt); 33 | api->godot_free(pkt); 34 | } 35 | q->last_pkt = NULL; 36 | q->first_pkt = NULL; 37 | q->nb_packets = 0; 38 | q->size = 0; 39 | } 40 | 41 | int packet_queue_put(PacketQueue *q, AVPacket *pkt) { 42 | 43 | AVPacketList *pkt1; 44 | pkt1 = (AVPacketList *)api->godot_alloc(sizeof(AVPacketList)); 45 | if (!pkt1) 46 | return -1; 47 | pkt1->pkt = *pkt; 48 | pkt1->next = NULL; 49 | 50 | if (!q->last_pkt) 51 | q->first_pkt = pkt1; 52 | else 53 | q->last_pkt->next = pkt1; 54 | q->last_pkt = pkt1; 55 | q->nb_packets++; 56 | q->size += pkt1->pkt.size; 57 | return 0; 58 | } 59 | 60 | int packet_queue_get(PacketQueue *q, AVPacket *pkt) { 61 | AVPacketList *pkt1; 62 | int ret; 63 | 64 | pkt1 = q->first_pkt; 65 | if (pkt1) { 66 | 67 | q->first_pkt = pkt1->next; 68 | if (!q->first_pkt) 69 | q->last_pkt = NULL; 70 | q->nb_packets--; 71 | q->size -= pkt1->pkt.size; 72 | *pkt = pkt1->pkt; 73 | api->godot_free(pkt1); 74 | return 1; 75 | } else { 76 | return 0; 77 | } 78 | } 79 | 80 | void packet_queue_deinit(PacketQueue *q) { 81 | AVPacket pt; 82 | while (packet_queue_get(q, &pt)) { 83 | av_packet_unref(&pt); 84 | } 85 | api->godot_free(q); 86 | } 87 | 88 | #endif /* _PACKET_QUEUE_H */ 89 | -------------------------------------------------------------------------------- /src/set.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef _STRING_SET_H 3 | #define _STRING_SET_H 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "linked_list.h" 11 | 12 | extern const godot_gdnative_core_api_struct *api; 13 | 14 | typedef struct set_bst_node_t { 15 | char *value; 16 | int priority; 17 | struct set_bst_node_t *left; 18 | struct set_bst_node_t *right; 19 | } set_t; 20 | 21 | set_t *set_create_node(const char *root_val) { 22 | set_t *root = (set_t *)api->godot_alloc(sizeof(set_t)); 23 | root->value = (char *)api->godot_alloc(strlen(root_val) + 1); 24 | root->priority = rand(); 25 | strcpy(root->value, root_val); 26 | root->left = NULL; 27 | root->right = NULL; 28 | return root; 29 | } 30 | 31 | set_t *cw_rot(set_t *root) { 32 | set_t *new_root = root->left; 33 | root->left = new_root->right; 34 | new_root->right = root; 35 | return new_root; 36 | } 37 | 38 | set_t *ccw_rot(set_t *root) { 39 | set_t *new_root = root->right; 40 | root->right = new_root->left; 41 | new_root->left = root; 42 | return new_root; 43 | } 44 | 45 | set_t *set_insert(set_t *root, const char *val) { 46 | if (root == NULL) { 47 | return set_create_node(val); 48 | } 49 | int cmp_val = strcmp(val, root->value); 50 | if (cmp_val == 0) { 51 | return root; 52 | } else if (cmp_val < 0) { 53 | root->left = set_insert(root->left, val); 54 | if (root->left->priority > root->priority) { 55 | root = cw_rot(root); 56 | } 57 | return root; 58 | } else { 59 | root->right = set_insert(root->right, val); 60 | if (root->right->priority > root->priority) { 61 | root = ccw_rot(root); 62 | } 63 | return root; 64 | } 65 | } 66 | 67 | void set_free(set_t *root) { 68 | if (root == NULL) { 69 | return; 70 | } 71 | set_free(root->right); 72 | set_free(root->left); 73 | api->godot_free(root->value); 74 | api->godot_free(root); 75 | } 76 | 77 | void set_print(set_t *root, int depth) { 78 | if (root == NULL) return; 79 | for (int i = 0; i < depth; i++) { 80 | printf("."); 81 | } 82 | printf("%s\n", root->value); 83 | set_print(root->left, depth + 1); 84 | set_print(root->right, depth + 1); 85 | } 86 | 87 | list_t set_create_list(set_t *root) { 88 | if (root == NULL) { 89 | list_t l; 90 | l.start = NULL; 91 | l.end = NULL; 92 | return l; 93 | } 94 | list_t left = set_create_list(root->left); 95 | list_t right = set_create_list(root->right); 96 | list_append(&left, root->value); 97 | list_join(&left, &right); 98 | return left; 99 | } 100 | 101 | #endif /* _STRING_SET_H */ 102 | -------------------------------------------------------------------------------- /test/.import/icon.png-487276ed1e3a0c39cad0279d744ee560.md5: -------------------------------------------------------------------------------- 1 | source_md5="ae7e641067601e2184afcade49abd283" 2 | dest_md5="666d00497ab80edb9a199bfa253dc2f5" 3 | 4 | -------------------------------------------------------------------------------- /test/.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kidrigger/godot-videodecoder/af71383dce66eea88f3ce99b416613b925be3ba0/test/.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex -------------------------------------------------------------------------------- /test/Node2D.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene format=2] 2 | 3 | [node name="Node2D" type="Node2D" index="0"] 4 | 5 | -------------------------------------------------------------------------------- /test/World.gd: -------------------------------------------------------------------------------- 1 | extends Control 2 | 3 | func _ready() -> void: 4 | var d := Directory.new() 5 | var mb := $MenuButton as MenuButton 6 | var p := mb.get_popup() 7 | if d.open('res://test_samples') == OK: 8 | d.list_dir_begin(true) 9 | var file_name = d.get_next() 10 | while file_name != '': 11 | if !file_name.ends_with('.txt'): 12 | p.add_item(file_name) 13 | file_name = d.get_next() 14 | if p.get_item_count(): 15 | _open(p.get_item_text(p.get_item_count() - 1)) 16 | var err = p.connect('index_pressed', self, '_on_menubutton_index_pressed') 17 | assert(err == OK) 18 | 19 | func _on_menubutton_index_pressed(index: int): 20 | _open($MenuButton.get_popup().get_item_text(index)) 21 | 22 | func _open(file): 23 | file = 'res://test_samples/%s' % [file] 24 | var stream = VideoStreamGDNative.new() 25 | print(file) 26 | stream.set_file(file) 27 | var vp = $VideoPlayer 28 | vp.stream = stream 29 | var sp = vp.stream_position 30 | # hack: to get the stream length, set the position to a negative number 31 | # the plugin will set the position to the end of the stream instead. 32 | vp.stream_position = -1 33 | var duration = vp.stream_position 34 | $ProgressBar.max_value = duration 35 | vp.stream_position = sp 36 | vp.play() 37 | 38 | func _on_VideoPlayer_finished(): 39 | get_tree().quit() 40 | 41 | func _process(delta: float) -> void: 42 | var pos = $VideoPlayer.stream_position 43 | $ProgressBar.value = pos 44 | $ProgressBar/Label.text = '%.f / %.f seconds' % [pos, $ProgressBar.max_value] 45 | -------------------------------------------------------------------------------- /test/World.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://World.gd" type="Script" id=2] 4 | 5 | [node name="World" type="Control"] 6 | anchor_right = 1.0 7 | anchor_bottom = 1.0 8 | script = ExtResource( 2 ) 9 | __meta__ = { 10 | "_edit_use_anchors_": false 11 | } 12 | 13 | [node name="VideoPlayer" type="VideoPlayer" parent="."] 14 | anchor_right = 1.0 15 | anchor_bottom = 1.0 16 | margin_bottom = -52.0526 17 | __meta__ = { 18 | "_edit_use_anchors_": false 19 | } 20 | 21 | [node name="ProgressBar" type="ProgressBar" parent="."] 22 | anchor_top = 1.0 23 | anchor_right = 1.0 24 | anchor_bottom = 1.0 25 | margin_left = 83.0 26 | margin_top = -48.0 27 | percent_visible = false 28 | __meta__ = { 29 | "_edit_use_anchors_": false 30 | } 31 | 32 | [node name="Label" type="Label" parent="ProgressBar"] 33 | anchor_right = 1.0 34 | anchor_bottom = 1.0 35 | align = 1 36 | valign = 1 37 | 38 | [node name="MenuButton" type="MenuButton" parent="."] 39 | anchor_top = 1.0 40 | anchor_bottom = 1.0 41 | margin_top = -48.0 42 | margin_right = 76.0 43 | text = "Open" 44 | -------------------------------------------------------------------------------- /test/addons/videodecoder.gdnlib: -------------------------------------------------------------------------------- 1 | [general] 2 | 3 | singleton=true 4 | load_once=true 5 | symbol_prefix="godot_" 6 | reloadable=false 7 | 8 | [entry] 9 | 10 | OSX.64="res://addons/bin/osx/libgdnative_videodecoder.dylib" 11 | Windows.64="res://addons/bin/win64/libgdnative_videodecoder.dll" 12 | Windows.32="res://addons/bin/win32/libgdnative_videodecoder.dll" 13 | X11.64="res://addons/bin/x11/libgdnative_videodecoder.so" 14 | X11.32="res://addons/bin/x11_32/libgdnative_videodecoder.so" 15 | 16 | 17 | [dependencies] 18 | 19 | OSX.64=[ "res://addons/bin/osx/libavformat.58.dylib", "res://addons/bin/osx/libavutil.56.dylib", "res://addons/bin/osx/libavcodec.58.dylib", "res://addons/bin/osx/libswscale.5.dylib", "res://addons/bin/osx/libswresample.3.dylib" ] 20 | Windows.64=[ "res://addons/bin/win64/avformat-58.dll", "res://addons/bin/win64/avutil-56.dll", "res://addons/bin/win64/avcodec-58.dll", "res://addons/bin/win64/swscale-5.dll", "res://addons/bin/win64/swresample-3.dll", "res://addons/bin/win64/libwinpthread-1.dll" ] 21 | Windows.32=[ "res://addons/bin/win32/avformat-58.dll", "res://addons/bin/win32/avutil-56.dll", "res://addons/bin/win32/avcodec-58.dll", "res://addons/bin/win64/swscale-5.dll", "res://addons/bin/win32/swresample-3.dll", "res://addons/bin/win32/libwinpthread-1.dll" ] 22 | X11.64=[ "res://addons/bin/x11/libavformat.so.58", "res://addons/bin/x11/libavutil.so.56", "res://addons/bin/x11/libavcodec.so.58", "res://addons/bin/x11/libswscale.so.5", "res://addons/bin/x11/libswresample.so.3" ] 23 | X11.32=[ "res://addons/bin/x11_32/libavformat.so.58", "res://addons/bin/x11_32/libavutil.so.56", "res://addons/bin/x11_32/libavcodec.so.58", "res://addons/bin/x11_32/libswscale.so.5", "res://addons/bin/x11_32/libswresample.so.3" ] 24 | -------------------------------------------------------------------------------- /test/default_env.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Environment" load_steps=2 format=2] 2 | 3 | [sub_resource type="ProceduralSky" id=1] 4 | radiance_size = 4 5 | sky_top_color = Color( 0.0470588, 0.454902, 0.976471, 1 ) 6 | sky_horizon_color = Color( 0.556863, 0.823529, 0.909804, 1 ) 7 | sky_curve = 0.25 8 | ground_bottom_color = Color( 0.101961, 0.145098, 0.188235, 1 ) 9 | ground_horizon_color = Color( 0.482353, 0.788235, 0.952941, 1 ) 10 | ground_curve = 0.01 11 | sun_energy = 16.0 12 | 13 | [resource] 14 | background_mode = 2 15 | background_sky = SubResource( 1 ) 16 | fog_height_min = 0.0 17 | fog_height_max = 100.0 18 | ssao_quality = 0 19 | -------------------------------------------------------------------------------- /test/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kidrigger/godot-videodecoder/af71383dce66eea88f3ce99b416613b925be3ba0/test/icon.png -------------------------------------------------------------------------------- /test/icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://icon.png" 13 | dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /test/project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=4 10 | 11 | _global_script_classes=[ ] 12 | _global_script_class_icons={ 13 | 14 | } 15 | 16 | [application] 17 | 18 | config/name="test" 19 | run/main_scene="res://World.tscn" 20 | config/icon="res://icon.png" 21 | 22 | [gdnative] 23 | 24 | singletons=[ "res://addons/videodecoder.gdnlib" ] 25 | 26 | [input] 27 | 28 | ui_accept={ 29 | "deadzone": 0.5, 30 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777221,"unicode":0,"echo":false,"script":null) 31 | , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777222,"unicode":0,"echo":false,"script":null) 32 | , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":32,"unicode":0,"echo":false,"script":null) 33 | , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":0,"pressure":0.0,"pressed":false,"script":null) 34 | ] 35 | } 36 | ui_select={ 37 | "deadzone": 0.5, 38 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":32,"unicode":0,"echo":false,"script":null) 39 | , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":3,"pressure":0.0,"pressed":false,"script":null) 40 | ] 41 | } 42 | ui_cancel={ 43 | "deadzone": 0.5, 44 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777217,"unicode":0,"echo":false,"script":null) 45 | , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":1,"pressure":0.0,"pressed":false,"script":null) 46 | ] 47 | } 48 | ui_focus_next={ 49 | "deadzone": 0.5, 50 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777218,"unicode":0,"echo":false,"script":null) 51 | ] 52 | } 53 | ui_focus_prev={ 54 | "deadzone": 0.5, 55 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":true,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777218,"unicode":0,"echo":false,"script":null) 56 | ] 57 | } 58 | ui_left={ 59 | "deadzone": 0.5, 60 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777231,"unicode":0,"echo":false,"script":null) 61 | , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":14,"pressure":0.0,"pressed":false,"script":null) 62 | ] 63 | } 64 | ui_right={ 65 | "deadzone": 0.5, 66 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777233,"unicode":0,"echo":false,"script":null) 67 | , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":15,"pressure":0.0,"pressed":false,"script":null) 68 | ] 69 | } 70 | ui_up={ 71 | "deadzone": 0.5, 72 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777232,"unicode":0,"echo":false,"script":null) 73 | , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":12,"pressure":0.0,"pressed":false,"script":null) 74 | ] 75 | } 76 | ui_down={ 77 | "deadzone": 0.5, 78 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777234,"unicode":0,"echo":false,"script":null) 79 | , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":13,"pressure":0.0,"pressed":false,"script":null) 80 | ] 81 | } 82 | ui_page_up={ 83 | "deadzone": 0.5, 84 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777235,"unicode":0,"echo":false,"script":null) 85 | ] 86 | } 87 | ui_page_down={ 88 | "deadzone": 0.5, 89 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777236,"unicode":0,"echo":false,"script":null) 90 | ] 91 | } 92 | ui_home={ 93 | "deadzone": 0.5, 94 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777229,"unicode":0,"echo":false,"script":null) 95 | ] 96 | } 97 | ui_end={ 98 | "deadzone": 0.5, 99 | "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777230,"unicode":0,"echo":false,"script":null) 100 | ] 101 | } 102 | 103 | [rendering] 104 | 105 | environment/default_environment="res://default_env.tres" 106 | -------------------------------------------------------------------------------- /test/test_samples/file_example_WEBM_480_900KB.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kidrigger/godot-videodecoder/af71383dce66eea88f3ce99b416613b925be3ba0/test/test_samples/file_example_WEBM_480_900KB.webm -------------------------------------------------------------------------------- /test/test_samples/out8.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kidrigger/godot-videodecoder/af71383dce66eea88f3ce99b416613b925be3ba0/test/test_samples/out8.webm -------------------------------------------------------------------------------- /test/test_samples/out9.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kidrigger/godot-videodecoder/af71383dce66eea88f3ce99b416613b925be3ba0/test/test_samples/out9.webm -------------------------------------------------------------------------------- /test/test_samples/sources.txt: -------------------------------------------------------------------------------- 1 | https://file-examples.com/index.php/sample-video-files/sample-webm-files-download/ (re-encoded from https://pixabay.com/pl/users/NASA-Imagery-10/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=2 ) 2 | https://www.base-n.de/webm/VP9%20Sample.html (re-encoded from https://media.xiph.org/video/derf/y4m/ ) 3 | --------------------------------------------------------------------------------