├── .ci ├── apple_arm64_x265.patch ├── apple_libvorbis_cpusubtype.patch ├── build-wheels.sh ├── build_wheels_osx.sh ├── dep_versions.sh ├── fdk.patch ├── libmp3lame-symbols.patch ├── merge_osx_deps.sh ├── x265_51ae8e922bcc4586ad4710812072289af91492a8.patch ├── x265_b354c009a60bcd6d7fc04014e200a1ee9c45c167.patch └── yum_deps.sh ├── .github └── workflows │ └── pythonapp.yml ├── .gitignore ├── COPYING ├── Makefile ├── README.rst ├── doc ├── Makefile ├── make.bat └── source │ ├── api.rst │ ├── conf.py │ ├── examples.rst │ ├── getting_started.rst │ ├── index.rst │ ├── installation.rst │ ├── pic.rst │ ├── player.rst │ ├── tools.rst │ └── writer.rst ├── examples ├── dw11222.mp4 ├── eye.gif └── test.py ├── ffpyplayer ├── __init__.py ├── clib │ ├── misc.c │ └── misc.h ├── includes │ ├── ff_consts.pxi │ ├── ffmpeg.pxi │ ├── inline_funcs.pxi │ └── sdl.pxi ├── pic.pxd ├── pic.pyx ├── player │ ├── __init__.py │ ├── clock.pxd │ ├── clock.pyx │ ├── core.pxd │ ├── core.pyx │ ├── decoder.pxd │ ├── decoder.pyx │ ├── frame_queue.pxd │ ├── frame_queue.pyx │ ├── player.pxd │ ├── player.pyx │ ├── queue.pxd │ └── queue.pyx ├── tests │ ├── __init__.py │ ├── common.py │ ├── test_pic.py │ ├── test_play.py │ └── test_write.py ├── threading.pxd ├── threading.pyx ├── tools.pyx ├── writer.pxd └── writer.pyx ├── pyproject.toml └── setup.py /.ci/apple_libvorbis_cpusubtype.patch: -------------------------------------------------------------------------------- 1 | --- configure.orig 2024-10-09 23:11:57 2 | +++ configure 2024-10-09 23:12:43 3 | @@ -12840,9 +12840,9 @@ 4 | CFLAGS="-O3 -Wall -Wextra -ffast-math -D__NO_MATH_INLINES -fsigned-char $sparc_cpu" 5 | PROFILE="-pg -g -O3 -D__NO_MATH_INLINES -fsigned-char $sparc_cpu" ;; 6 | *-*-darwin*) 7 | - DEBUG="-DDARWIN -fno-common -force_cpusubtype_ALL -Wall -g -O0 -fsigned-char" 8 | - CFLAGS="-DDARWIN -fno-common -force_cpusubtype_ALL -Wall -g -O3 -ffast-math -fsigned-char" 9 | - PROFILE="-DDARWIN -fno-common -force_cpusubtype_ALL -Wall -g -pg -O3 -ffast-math -fsigned-char";; 10 | + DEBUG="-DDARWIN -fno-common -Wall -g -O0 -fsigned-char" 11 | + CFLAGS="-DDARWIN -fno-common -Wall -g -O3 -ffast-math -fsigned-char" 12 | + PROFILE="-DDARWIN -fno-common -Wall -g -pg -O3 -ffast-math -fsigned-char";; 13 | *-*-os2*) 14 | # Use -W instead of -Wextra because gcc on OS/2 is an old version. 15 | DEBUG="-g -Wall -W -D_REENTRANT -D__NO_MATH_INLINES -fsigned-char" -------------------------------------------------------------------------------- /.ci/build-wheels.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e -x 3 | 4 | # no permissions in that dir 5 | source /io/.ci/yum_deps.sh 6 | source /io/.ci/dep_versions.sh 7 | 8 | BUILD_DIR="$HOME/ffmpeg_build" 9 | export LD_LIBRARY_PATH="$BUILD_DIR/lib:$LD_LIBRARY_PATH" 10 | export PATH="$BUILD_DIR/bin:$PATH" 11 | export PKG_CONFIG_PATH="$BUILD_DIR/lib/pkgconfig:$BUILD_DIR/lib64/pkgconfig:/usr/lib/pkgconfig/" 12 | 13 | mkdir ~/ffmpeg_sources 14 | 15 | 16 | cd ~/ffmpeg_sources; 17 | curl -sLO "https://github.com/libsdl-org/SDL/releases/download/release-$SDL_VERSION/SDL2-$SDL_VERSION.tar.gz" 18 | tar xzf "SDL2-$SDL_VERSION.tar.gz" 19 | cd "SDL2-$SDL_VERSION" 20 | ./configure --prefix="$BUILD_DIR" --bindir="$BUILD_DIR/bin"; 21 | make; 22 | make install; 23 | make distclean; 24 | 25 | cd ~/ffmpeg_sources; 26 | curl -sLO "https://www.openssl.org/source/openssl-$OPENSSL_VERSION.tar.gz" 27 | tar xzf "openssl-$OPENSSL_VERSION.tar.gz" 28 | cd "openssl-$OPENSSL_VERSION" 29 | ./config -fpic shared --prefix="$BUILD_DIR"; 30 | make; 31 | make install; 32 | 33 | cd ~/ffmpeg_sources; 34 | curl -sLO "http://www.tortall.net/projects/yasm/releases/yasm-$YASM_VERSION.tar.gz" 35 | tar xzf "yasm-$YASM_VERSION.tar.gz" 36 | cd "yasm-$YASM_VERSION" 37 | ./configure --prefix="$BUILD_DIR" --bindir="$BUILD_DIR/bin"; 38 | make; 39 | make install; 40 | make distclean; 41 | 42 | cd ~/ffmpeg_sources; 43 | curl -sLO "http://www.nasm.us/pub/nasm/releasebuilds/$NASM_VERSION/nasm-$NASM_VERSION.tar.gz" 44 | tar -xvzf "nasm-$NASM_VERSION.tar.gz" 45 | cd "nasm-$NASM_VERSION" 46 | ./configure --prefix="$BUILD_DIR" --bindir="$BUILD_DIR/bin"; 47 | make; 48 | make install; 49 | make distclean; 50 | 51 | cd ~/ffmpeg_sources; 52 | git clone --depth 1 --branch stable https://code.videolan.org/videolan/x264.git 53 | cd x264 54 | ./configure --prefix="$BUILD_DIR" --bindir="$BUILD_DIR/bin" --enable-shared --extra-cflags="-fPIC"; 55 | make; 56 | make install; 57 | make distclean; 58 | 59 | cd ~/ffmpeg_sources; 60 | curl -kLO "https://cfhcable.dl.sourceforge.net/project/lame/lame/$LAME_VERSION/lame-$LAME_VERSION.tar.gz" 61 | tar xzf "lame-$LAME_VERSION.tar.gz" 62 | cd "lame-$LAME_VERSION" 63 | ./configure --prefix="$BUILD_DIR" --enable-nasm --enable-shared; 64 | make; 65 | make install; 66 | make distclean; 67 | 68 | cd ~/ffmpeg_sources 69 | curl -sLO "https://github.com/fribidi/fribidi/releases/download/v$FRIBIDI_VERSION/fribidi-$FRIBIDI_VERSION.tar.xz" 70 | tar xf "fribidi-$FRIBIDI_VERSION.tar.xz" 71 | cd "fribidi-$FRIBIDI_VERSION" 72 | ./configure --prefix="$BUILD_DIR" --enable-shared; 73 | make 74 | make install 75 | 76 | cd ~/ffmpeg_sources 77 | curl -sLO "https://github.com/libass/libass/releases/download/$LIBASS_VERSION/libass-$LIBASS_VERSION.tar.gz" 78 | tar xzf "libass-$LIBASS_VERSION.tar.gz" 79 | cd "libass-$LIBASS_VERSION" 80 | ./configure --prefix="$BUILD_DIR" --enable-shared --disable-require-system-font-provider; 81 | make 82 | make install 83 | 84 | cd ~/ffmpeg_sources 85 | curl -sLO "https://bitbucket.org/multicoreware/x265_git/downloads/x265_$X265_VERSION.tar.gz" 86 | tar xzf "x265_$X265_VERSION.tar.gz" 87 | cd x265_*/ 88 | # Backport patches to fix build on cmake >4.0.0 89 | patch -p1 < /io/.ci/x265_b354c009a60bcd6d7fc04014e200a1ee9c45c167.patch 90 | patch -p1 < /io/.ci/x265_51ae8e922bcc4586ad4710812072289af91492a8.patch 91 | cd build/linux 92 | cmake -G "Unix Makefiles" -DCMAKE_INSTALL_PREFIX="$BUILD_DIR" -DENABLE_SHARED:bool=on ../../source 93 | make 94 | make install 95 | 96 | cd ~/ffmpeg_sources 97 | git clone --depth 1 --branch "v$FDK_VERSION" https://github.com/mstorsjo/fdk-aac.git 98 | cd fdk-aac 99 | git apply /io/.ci/fdk.patch 100 | autoreconf -fiv 101 | ./configure --prefix="$BUILD_DIR" --enable-shared 102 | make 103 | make install 104 | 105 | cd ~/ffmpeg_sources 106 | curl -LO "https://archive.mozilla.org/pub/opus/opus-$OPUS_VERSION.tar.gz" 107 | tar xzvf "opus-$OPUS_VERSION.tar.gz" 108 | cd "opus-$OPUS_VERSION" 109 | ./configure --prefix="$BUILD_DIR" --enable-shared 110 | make 111 | make install 112 | 113 | cd ~/ffmpeg_sources 114 | curl -LO "http://downloads.xiph.org/releases/ogg/libogg-$LIBOGG_VERSION.tar.gz" 115 | tar xzvf "libogg-$LIBOGG_VERSION.tar.gz" 116 | cd "libogg-$LIBOGG_VERSION" 117 | ./configure --prefix="$BUILD_DIR" --enable-shared 118 | make 119 | make install 120 | 121 | cd ~/ffmpeg_sources; 122 | curl -LO "http://downloads.xiph.org/releases/theora/libtheora-$LIBTHEORA_VERSION.tar.gz" 123 | tar xzvf "libtheora-$LIBTHEORA_VERSION.tar.gz" 124 | cd "libtheora-$LIBTHEORA_VERSION" 125 | ./configure --prefix="$BUILD_DIR" --enable-shared; 126 | make; 127 | make install 128 | 129 | cd ~/ffmpeg_sources 130 | curl -LO "http://downloads.xiph.org/releases/vorbis/libvorbis-$LIBVORBIS_VERSION.tar.gz" 131 | tar xzvf "libvorbis-$LIBVORBIS_VERSION.tar.gz" 132 | cd "libvorbis-$LIBVORBIS_VERSION" 133 | ./configure --prefix="$BUILD_DIR" --with-ogg="$BUILD_DIR" --enable-shared 134 | make 135 | make install 136 | 137 | cd ~/ffmpeg_sources 138 | git clone --depth 1 --branch "v$LIBVPX_VERSION" https://chromium.googlesource.com/webm/libvpx.git 139 | cd libvpx 140 | ./configure --prefix="$BUILD_DIR" --disable-examples --as=yasm --enable-shared --disable-unit-tests 141 | make 142 | make install 143 | 144 | cd ~/ffmpeg_sources; 145 | curl -sLO http://ffmpeg.org/releases/ffmpeg-$FFMPEG_VERSION.tar.bz2; 146 | tar xjf ffmpeg-$FFMPEG_VERSION.tar.bz2; 147 | cd ffmpeg-$FFMPEG_VERSION; 148 | ./configure --prefix="$BUILD_DIR" --extra-cflags="-I$BUILD_DIR/include -fPIC" --extra-ldflags="-L$BUILD_DIR/lib" --bindir="$BUILD_DIR/bin" --enable-gpl --enable-version3 --enable-libmp3lame --enable-libx264 --enable-libx265 --enable-libfdk_aac --enable-nonfree --enable-libass --enable-libvorbis --enable-libtheora --enable-libfreetype --enable-libopus --enable-libvpx --enable-openssl --enable-shared 149 | make; 150 | make install; 151 | 152 | cp -r "$BUILD_DIR" "/io/ffmpeg_build" 153 | -------------------------------------------------------------------------------- /.ci/build_wheels_osx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e -x 3 | 4 | # can be either arm64 or x86_64 5 | ARCH="$1" 6 | SRC_PATH="$HOME/ffmpeg_sources_$ARCH" 7 | BUILD_PATH="$HOME/${FFMPEG_BUILD_PATH}_$ARCH" 8 | base_dir="$(pwd)" 9 | 10 | source "$base_dir/.ci/dep_versions.sh" 11 | 12 | export LD_LIBRARY_PATH="$BUILD_PATH/lib:$LD_LIBRARY_PATH" 13 | export PATH="$BUILD_PATH/bin:/usr/local/bin/:$PATH" 14 | export PKG_CONFIG_PATH="$BUILD_PATH/lib/pkgconfig:/usr/lib/pkgconfig/:$PKG_CONFIG_PATH" 15 | export CC="/usr/bin/clang" 16 | export CXX="/usr/bin/clang" 17 | export MACOSX_DEPLOYMENT_TARGET=10.13 18 | 19 | if [ "$ARCH" = "x86_64" ]; then 20 | ARCH2=x86_64 21 | else 22 | ARCH2=aarch64 23 | export CFLAGS="-arch arm64" 24 | export CXXFLAGS="-arch arm64" 25 | fi 26 | 27 | 28 | brew install automake meson pkg-config cmake 29 | brew install --cask xquartz 30 | mkdir "$SRC_PATH" 31 | 32 | 33 | cd "$SRC_PATH" 34 | curl -sLO "https://tukaani.org/xz/xz-$XZ_VERSION.tar.gz" 35 | tar xzf "xz-$XZ_VERSION.tar.gz" 36 | cd "xz-$XZ_VERSION" 37 | ./configure --prefix="$BUILD_PATH" --host=$ARCH2-darwin 38 | make 39 | make install 40 | 41 | 42 | cd "$SRC_PATH" 43 | curl -sLO "https://zlib.net/fossils/zlib-$ZLIB_VERSION.tar.gz" 44 | tar xzf "zlib-$ZLIB_VERSION.tar.gz" 45 | cd "zlib-$ZLIB_VERSION" 46 | ./configure --prefix="$BUILD_PATH" 47 | make 48 | make install 49 | 50 | 51 | cd "$SRC_PATH"; 52 | curl -sLO "https://github.com/libsdl-org/SDL/releases/download/release-$SDL_VERSION/SDL2-$SDL_VERSION.tar.gz" 53 | tar xzf "SDL2-$SDL_VERSION.tar.gz" 54 | cd "SDL2-$SDL_VERSION" 55 | CPPFLAGS="$CXXFLAGS" LDFLAGS="$CFLAGS" ./configure --prefix="$BUILD_PATH" --bindir="$BUILD_PATH/bin" --host=$ARCH2-darwin 56 | make 57 | make install 58 | make distclean 59 | 60 | 61 | cd "$SRC_PATH" 62 | curl -sLO "https://www.openssl.org/source/openssl-$OPENSSL_VERSION.tar.gz" 63 | tar xzf "openssl-$OPENSSL_VERSION.tar.gz" 64 | cd "openssl-$OPENSSL_VERSION" 65 | ./configure darwin64-$ARCH-cc -fPIC shared --prefix="$BUILD_PATH" 66 | make 67 | make install 68 | 69 | 70 | cd "$SRC_PATH" 71 | curl -sLO "https://github.com/glennrp/libpng/archive/refs/tags/v$LIBPNG_VERSION.tar.gz" 72 | tar xzf "v$LIBPNG_VERSION.tar.gz" 73 | cd "libpng-$LIBPNG_VERSION" 74 | ./configure --prefix="$BUILD_PATH" --bindir="$BUILD_PATH/bin" --host=$ARCH2-darwin 75 | make 76 | make install 77 | 78 | 79 | cd "$SRC_PATH" 80 | curl -sLO "https://github.com/google/brotli/archive/refs/tags/v$BROTLI_VERSION.tar.gz" 81 | tar xzf "v$BROTLI_VERSION.tar.gz" 82 | cd "brotli-$BROTLI_VERSION" 83 | mkdir out 84 | cd out 85 | cmake -DCMAKE_INSTALL_PREFIX="$BUILD_PATH" -DCMAKE_OSX_ARCHITECTURES="$ARCH" -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 .. 86 | cmake --build . --config Release --target install 87 | 88 | 89 | if [ "$ARCH" = "x86_64" ]; then 90 | cd "$SRC_PATH" 91 | curl -sLO "http://www.tortall.net/projects/yasm/releases/yasm-$YASM_VERSION.tar.gz" 92 | tar xzf "yasm-$YASM_VERSION.tar.gz" 93 | cd "yasm-$YASM_VERSION" 94 | ./configure --prefix="$BUILD_PATH" --bindir="$BUILD_PATH/bin" 95 | make 96 | make install 97 | make distclean 98 | 99 | cd "$SRC_PATH" 100 | curl -sLO "http://www.nasm.us/pub/nasm/releasebuilds/$NASM_VERSION/nasm-$NASM_VERSION.tar.gz" 101 | tar -xvzf "nasm-$NASM_VERSION.tar.gz" 102 | cd "nasm-$NASM_VERSION" 103 | ./configure --prefix="$BUILD_PATH" --bindir="$BUILD_PATH/bin" 104 | make 105 | make install 106 | make distclean 107 | 108 | fi 109 | 110 | 111 | arg=() 112 | if [ "$ARCH" = "arm64" ]; then 113 | arg=("--disable-asm") 114 | fi 115 | cd "$SRC_PATH" 116 | git clone --depth 1 --branch stable https://code.videolan.org/videolan/x264.git 117 | cd x264 118 | ./configure --prefix="$BUILD_PATH" --bindir="$BUILD_PATH/bin" --enable-shared --extra-cflags="-fPIC" \ 119 | "${arg[@]}" --host=$ARCH2-darwin 120 | make 121 | make install 122 | make distclean 123 | 124 | 125 | arg=() 126 | if [ "$ARCH" = "x86_64" ]; then 127 | arg=("--enable-nasm") 128 | fi 129 | cd "$SRC_PATH"; 130 | curl -kLO "https://cfhcable.dl.sourceforge.net/project/lame/lame/$LAME_VERSION/lame-$LAME_VERSION.tar.gz" 131 | tar xzf "lame-$LAME_VERSION.tar.gz" 132 | cd "lame-$LAME_VERSION" 133 | git apply "$base_dir/.ci/libmp3lame-symbols.patch" 134 | ./configure --prefix="$BUILD_PATH" --enable-shared "${arg[@]}" --host=$ARCH2-darwin 135 | make 136 | make install 137 | make distclean 138 | 139 | 140 | cd "$SRC_PATH" 141 | curl -sLO "https://github.com/fribidi/fribidi/releases/download/v$FRIBIDI_VERSION/fribidi-$FRIBIDI_VERSION.tar.xz" 142 | tar xf "fribidi-$FRIBIDI_VERSION.tar.xz" 143 | cd "fribidi-$FRIBIDI_VERSION" 144 | ./configure --prefix="$BUILD_PATH" --enable-shared --host=$ARCH2-darwin 145 | make 146 | make install 147 | 148 | 149 | cd "$SRC_PATH" 150 | curl -sLO "https://download.savannah.gnu.org/releases/freetype/freetype-$FREETYPE_VERSION.tar.xz" 151 | tar xf "freetype-$FREETYPE_VERSION.tar.xz" 152 | cd "freetype-$FREETYPE_VERSION" 153 | ./configure --prefix="$BUILD_PATH" --enable-shared --host=$ARCH2-darwin --with-harfbuzz=no 154 | make 155 | make install 156 | 157 | 158 | cd "$SRC_PATH" 159 | curl -sLO "https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/harfbuzz-$HARFBUZZ_VERSION.tar.xz" 160 | tar xf "harfbuzz-$HARFBUZZ_VERSION.tar.xz" 161 | cd "harfbuzz-$HARFBUZZ_VERSION" 162 | 163 | 164 | if [ "$ARCH" = "arm64" ]; then 165 | cat < cross_file.txt 166 | [host_machine] 167 | system = 'darwin' 168 | cpu_family = 'aarch64' 169 | cpu = 'arm64' 170 | endian = 'little' 171 | [binaries] 172 | pkgconfig = '/usr/local/bin/pkg-config' 173 | EOT 174 | 175 | LDFLAGS="-arch arm64" meson build --prefix="$BUILD_PATH" -Dglib=disabled -Dgobject=disabled -Dcairo=disabled \ 176 | -Dfreetype=enabled -Ddocs=disabled -Dtests=disabled -Dintrospection=disabled -Dbenchmark=disabled \ 177 | --cross-file cross_file.txt -Dc_args="-arch arm64" -Dc_link_args="-arch arm64" -Dcpp_args="-arch arm64" \ 178 | -Dcpp_link_args="-arch arm64" 179 | LDFLAGS="-arch arm64" meson compile -C build 180 | else 181 | meson build --prefix="$BUILD_PATH" -Dglib=disabled -Dgobject=disabled -Dcairo=disabled -Dfreetype=enabled \ 182 | -Ddocs=disabled -Dtests=disabled -Dintrospection=disabled -Dbenchmark=disabled 183 | meson compile -C build 184 | fi 185 | meson install -C build 186 | 187 | 188 | cd "$SRC_PATH" 189 | curl -sLO "https://github.com/libass/libass/releases/download/$LIBASS_VERSION/libass-$LIBASS_VERSION.tar.gz" 190 | tar xzf "libass-$LIBASS_VERSION.tar.gz" 191 | cd "libass-$LIBASS_VERSION" 192 | ./configure --prefix="$BUILD_PATH" --enable-shared --disable-fontconfig --host=$ARCH2-darwin 193 | make 194 | make install 195 | 196 | 197 | cd "$SRC_PATH" 198 | git clone https://bitbucket.org/multicoreware/x265_git.git --depth 1 --branch "Release_$X265_VERSION" 199 | cd x265_git 200 | # Backport patches to fix build on cmake >4.0.0 201 | patch -p1 < "$base_dir/.ci/x265_b354c009a60bcd6d7fc04014e200a1ee9c45c167.patch" 202 | patch -p1 < "$base_dir/.ci/x265_51ae8e922bcc4586ad4710812072289af91492a8.patch" 203 | if [ "$ARCH" = "arm64" ]; then 204 | patch -p1 < "$base_dir/.ci/apple_arm64_x265.patch" 205 | cd source 206 | sed -i "" "s/^if(X265_LATEST_TAG)$/if(1)/g" CMakeLists.txt 207 | CXX= LDFLAGS="-arch arm64" cmake -G "Unix Makefiles" -DCMAKE_INSTALL_PREFIX="$BUILD_PATH" -DENABLE_SHARED:bool=on \ 208 | -DCMAKE_OSX_ARCHITECTURES=arm64 -DCROSS_COMPILE_ARM64:bool=on -DCMAKE_HOST_SYSTEM_PROCESSOR=aarch64 \ 209 | -DCMAKE_APPLE_SILICON_PROCESSOR=aarch64 . 210 | CXX= LDFLAGS="-arch arm64" make 211 | else 212 | cd source 213 | sed -i "" "s/^if(X265_LATEST_TAG)$/if(1)/g" CMakeLists.txt 214 | CXX= cmake -G "Unix Makefiles" -DCMAKE_INSTALL_PREFIX="$BUILD_PATH" -DENABLE_SHARED:bool=on . 215 | CXX= make 216 | fi 217 | make install 218 | 219 | 220 | cd "$SRC_PATH" 221 | git clone --depth 1 --branch "v$FDK_VERSION" https://github.com/mstorsjo/fdk-aac.git 222 | cd fdk-aac 223 | git apply "$base_dir/.ci/fdk.patch" 224 | cmake -DCMAKE_INSTALL_PREFIX="$BUILD_PATH" -DENABLE_SHARED:bool=on -DCMAKE_OSX_ARCHITECTURES="$ARCH" . 225 | make 226 | make install 227 | 228 | 229 | cd "$SRC_PATH" 230 | curl -LO "https://archive.mozilla.org/pub/opus/opus-$OPUS_VERSION.tar.gz" 231 | tar xzvf "opus-$OPUS_VERSION.tar.gz" 232 | cd "opus-$OPUS_VERSION" 233 | ./configure --prefix="$BUILD_PATH" --enable-shared --host=$ARCH2-darwin 234 | make 235 | make install 236 | 237 | 238 | cd "$SRC_PATH" 239 | curl -LO "http://downloads.xiph.org/releases/ogg/libogg-$LIBOGG_VERSION.tar.gz" 240 | tar xzvf "libogg-$LIBOGG_VERSION.tar.gz" 241 | cd "libogg-$LIBOGG_VERSION" 242 | ./configure --prefix="$BUILD_PATH" --enable-shared --host=$ARCH2-darwin 243 | make 244 | make install 245 | 246 | 247 | cd "$SRC_PATH" 248 | curl -LO "http://downloads.xiph.org/releases/vorbis/libvorbis-$LIBVORBIS_VERSION.tar.gz" 249 | tar xzvf "libvorbis-$LIBVORBIS_VERSION.tar.gz" 250 | cd "libvorbis-$LIBVORBIS_VERSION" 251 | patch -p1 < "$base_dir/.ci/apple_libvorbis_cpusubtype.patch" 252 | ./configure --prefix="$BUILD_PATH" --with-ogg="$BUILD_PATH" --enable-shared --host=$ARCH2-darwin 253 | make 254 | make install 255 | 256 | 257 | cd "$SRC_PATH"; 258 | curl -LO "http://downloads.xiph.org/releases/theora/libtheora-$LIBTHEORA_VERSION.tar.gz" 259 | tar xzvf "libtheora-$LIBTHEORA_VERSION.tar.gz" 260 | cd "libtheora-$LIBTHEORA_VERSION" 261 | # https://bugs.gentoo.org/465450 262 | sed -i "" 's/png_\(sizeof\)/\1/g' examples/png2theora.c 263 | THEORA_ARCH=$ARCH2 264 | ./configure --prefix="$BUILD_PATH" --enable-shared --host=$THEORA_ARCH-apple-darwin 265 | make 266 | make install 267 | 268 | 269 | cd "$SRC_PATH" 270 | git clone --depth 1 --branch "v$LIBVPX_VERSION" https://chromium.googlesource.com/webm/libvpx.git 271 | cd libvpx 272 | sed -i.original -e 's/-march=armv8-a//g' build/make/configure.sh 273 | 274 | if [ "$ARCH" = "x86_64" ]; then 275 | arg=("--as=yasm") 276 | LDFLAGS_VPX="$LDFLAGS" 277 | else 278 | arg=("--target=$ARCH-darwin20-gcc") 279 | LDFLAGS_VPX="$LDFLAGS -arch arm64" 280 | fi 281 | CXX= CC= LDFLAGS="$LDFLAGS_VPX" ./configure --prefix="$BUILD_PATH" --disable-examples --enable-vp9-highbitdepth --enable-vp8 --enable-vp9 --enable-pic \ 282 | --enable-postproc --enable-multithread "${arg[@]}" --enable-shared --disable-unit-tests 283 | CXX= CC= make 284 | make install 285 | 286 | 287 | cd "$SRC_PATH" 288 | curl -sLO "http://ffmpeg.org/releases/ffmpeg-$FFMPEG_VERSION.tar.bz2" 289 | tar xjf "ffmpeg-$FFMPEG_VERSION.tar.bz2" 290 | cd "ffmpeg-$FFMPEG_VERSION" 291 | 292 | if [ "$ARCH" = "x86_64" ]; then 293 | arg=("--extra-ldflags=-L$BUILD_PATH/lib") 294 | else 295 | arg=("--enable-cross-compile" "--arch=arm64" "--target-os=darwin" "--extra-ldflags=-L$BUILD_PATH/lib -arch arm64" \ 296 | "--extra-objcflags=-arch arm64") 297 | fi 298 | 299 | ./configure --prefix="$BUILD_PATH" --extra-cflags="$CFLAGS" --extra-cxxflags="$CXXFLAGS" --bindir="$BUILD_PATH/bin" \ 300 | --enable-gpl --enable-libmp3lame --enable-libx264 --enable-libx265 --enable-libfdk_aac --enable-nonfree \ 301 | --enable-libass --enable-libvorbis --enable-libtheora --enable-libfreetype --enable-libopus --enable-libvpx \ 302 | --enable-openssl --enable-shared --pkg-config-flags="--static" --disable-libxcb --disable-libxcb-shm \ 303 | --disable-libxcb-xfixes --disable-libxcb-shape --disable-xlib "${arg[@]}" 304 | make 305 | make install 306 | make distclean 307 | 308 | 309 | file "$BUILD_PATH"/lib/* 310 | file "$BUILD_PATH"/bin/* 311 | find "$BUILD_PATH" 312 | -------------------------------------------------------------------------------- /.ci/dep_versions.sh: -------------------------------------------------------------------------------- 1 | # FFMPEG_VERSION and SDL_VERSION are also set in the actions yaml 2 | 3 | export FFMPEG_VERSION=6.0 # https://ffmpeg.org/releases/ 4 | export SDL_VERSION=2.26.4 # https://github.com/libsdl-org/SDL/releases 5 | export SDL_MIXER_VERSION=2.6.3 # https://github.com/libsdl-org/SDL_mixer/releases 6 | export OPENSSL_VERSION=3.0.8 # https://www.openssl.org/source 7 | export YASM_VERSION=1.3.0 # http://www.tortall.net/projects/yasm/releases 8 | export NASM_VERSION=2.16.01 # https://www.nasm.us/pub/nasm/releasebuilds/ 9 | export LAME_VERSION=3.100 # https://sourceforge.net/projects/lame/files/lame/ 10 | export FRIBIDI_VERSION=1.0.12 # https://github.com/fribidi/fribidi/releases 11 | export LIBASS_VERSION=0.17.0 # https://github.com/libass/libass/releases 12 | export X265_VERSION=3.5 # https://bitbucket.org/multicoreware/x265_git/downloads 13 | export FDK_VERSION=2.0.2 # https://github.com/mstorsjo/fdk-aac 14 | export OPUS_VERSION=1.3.1 # https://archive.mozilla.org/pub/opus/ 15 | export LIBOGG_VERSION=1.3.5 # http://downloads.xiph.org/releases/ogg/ 16 | export LIBTHEORA_VERSION=1.2.0 # https://ftp.osuosl.org/pub/xiph/releases/theora/ 17 | export LIBVORBIS_VERSION=1.3.7 # http://downloads.xiph.org/releases/vorbis 18 | export LIBVPX_VERSION=1.14.1 # https://chromium.googlesource.com/webm/libvpx 19 | export XZ_VERSION=5.4.1 # https://tukaani.org/xz/ 20 | export ZLIB_VERSION=1.2.13 # https://zlib.net/ 21 | export LIBPNG_VERSION=1.6.39 # https://github.com/glennrp/libpng/tags 22 | export BROTLI_VERSION=1.0.9 # https://github.com/google/brotli/tags 23 | export FREETYPE_VERSION=2.12.1 # https://download.savannah.gnu.org/releases/freetype/ 24 | export HARFBUZZ_VERSION=6.0.0 # https://github.com/harfbuzz/harfbuzz/releases 25 | -------------------------------------------------------------------------------- /.ci/fdk.patch: -------------------------------------------------------------------------------- 1 | diff --git a/Makefile.am b/Makefile.am 2 | index 5b2c65b..728c72a 100644 3 | --- a/Makefile.am 4 | +++ b/Makefile.am 5 | @@ -13,7 +13,7 @@ AM_CPPFLAGS = \ 6 | -I$(top_srcdir)/libPCMutils/include 7 | 8 | AM_CXXFLAGS = -fno-exceptions -fno-rtti 9 | -libfdk_aac_la_LINK = $(LINK) $(libfdk_aac_la_LDFLAGS) 10 | +#libfdk_aac_la_LINK = $(LINK) $(libfdk_aac_la_LDFLAGS) 11 | # Mention a dummy pure C file to trigger generation of the $(LINK) variable 12 | nodist_EXTRA_libfdk_aac_la_SOURCES = dummy.c 13 | 14 | diff --git a/configure.ac b/configure.ac 15 | index 1485ff7..4bec7a7 100644 16 | --- a/configure.ac 17 | +++ b/configure.ac 18 | @@ -19,7 +19,11 @@ AM_CONDITIONAL(EXAMPLE, test x$example = xyes) 19 | dnl Checks for programs. 20 | AC_PROG_CC 21 | AC_PROG_CXX 22 | -LT_INIT 23 | + 24 | +AM_PROG_CC_C_O 25 | + 26 | +AC_PROG_LIBTOOL 27 | +AC_SUBST(LIBTOOL_DEPS) 28 | 29 | AC_SEARCH_LIBS([sin], [m]) 30 | 31 | -------------------------------------------------------------------------------- /.ci/libmp3lame-symbols.patch: -------------------------------------------------------------------------------- 1 | --- lame-3.100/include/libmp3lame.sym 2017-09-06 14:33:35.000000000 -0500 2 | +++ lame-3.100/include/libmp3lame.sym 2017-10-22 16:18:44.708436200 -0500 3 | @@ -1,5 +1,4 @@ 4 | lame_init 5 | -lame_init_old 6 | lame_set_num_samples 7 | lame_get_num_samples 8 | lame_set_in_samplerate 9 | @@ -188,6 +187,7 @@ hip_decode_exit 10 | hip_set_errorf 11 | hip_set_debugf 12 | hip_set_msgf 13 | +hip_set_pinfo 14 | hip_decode 15 | hip_decode_headers 16 | hip_decode1 17 | -------------------------------------------------------------------------------- /.ci/merge_osx_deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -x 4 | 5 | py_osx_ver=$(echo ${MACOSX_DEPLOYMENT_TARGET} | sed "s/\./_/g") 6 | py_osx_ver_arm=$(echo ${MACOSX_DEPLOYMENT_TARGET_ARM} | sed "s/\./_/g") 7 | for whl in *.whl; do 8 | if [[ "$whl" == *macosx_${py_osx_ver}_x86_64.whl ]]; then 9 | whl_base=$(echo "$whl" | rev | cut -c 24- | rev) 10 | if [[ -f "${whl_base}macosx_${py_osx_ver_arm}_arm64.whl" ]]; then 11 | delocate-merge "$whl" "${whl_base}macosx_${py_osx_ver_arm}_arm64.whl" 12 | fi 13 | fi 14 | done 15 | -------------------------------------------------------------------------------- /.ci/x265_51ae8e922bcc4586ad4710812072289af91492a8.patch: -------------------------------------------------------------------------------- 1 | From 51ae8e922bcc4586ad4710812072289af91492a8 Mon Sep 17 00:00:00 2001 2 | From: yaswanthsastry 3 | Date: Mon, 7 Apr 2025 11:27:36 +0530 4 | Subject: [PATCH] Fix for CMake Build Errors in MacOS 5 | 6 | --- 7 | source/CMakeLists.txt | 15 +++++++-------- 8 | 1 file changed, 7 insertions(+), 8 deletions(-) 9 | 10 | diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt 11 | index 4f5b3ed82..7183fd3ce 100755 12 | --- a/source/CMakeLists.txt 13 | +++ b/source/CMakeLists.txt 14 | @@ -6,18 +6,14 @@ if(NOT CMAKE_BUILD_TYPE) 15 | FORCE) 16 | endif() 17 | message(STATUS "cmake version ${CMAKE_VERSION}") 18 | -if(POLICY CMP0025) 19 | - cmake_policy(SET CMP0025 NEW) # report Apple's Clang as just Clang 20 | -endif() 21 | + 22 | if(POLICY CMP0042) 23 | cmake_policy(SET CMP0042 NEW) # MACOSX_RPATH 24 | endif() 25 | -if(POLICY CMP0054) 26 | - cmake_policy(SET CMP0054 NEW) # Only interpret if() arguments as variables or keywords when unquoted 27 | -endif() 28 | + 29 | 30 | project (x265) 31 | -cmake_minimum_required (VERSION 2.8.8) # OBJECT libraries require 2.8.8 32 | +cmake_minimum_required (VERSION 2.8.8...3.10) # OBJECT libraries require 2.8.8 33 | include(CheckIncludeFiles) 34 | include(CheckFunctionExists) 35 | include(CheckSymbolExists) 36 | @@ -168,7 +164,7 @@ if(APPLE) 37 | add_definitions(-DMACOS=1) 38 | endif() 39 | 40 | -if(${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") 41 | +if(${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang" OR ${CMAKE_CXX_COMPILER_ID} STREQUAL "AppleClang") 42 | set(CLANG 1) 43 | endif() 44 | if(${CMAKE_CXX_COMPILER_ID} STREQUAL "Intel") 45 | @@ -740,6 +736,9 @@ if((MSVC_IDE OR XCODE OR GCC) AND ENABLE_ASSEMBLY) 46 | if(ARM OR CROSS_COMPILE_ARM) 47 | # compile ARM arch asm files here 48 | enable_language(ASM) 49 | + if(APPLE) 50 | + set(ARM_ARGS ${ARM_ARGS} -arch ${CMAKE_OSX_ARCHITECTURES}) 51 | + endif() 52 | foreach(ASM ${ARM_ASMS}) 53 | set(ASM_SRC ${CMAKE_CURRENT_SOURCE_DIR}/common/arm/${ASM}) 54 | list(APPEND ASM_SRCS ${ASM_SRC}) 55 | -- 56 | 2.49.0 57 | 58 | -------------------------------------------------------------------------------- /.ci/x265_b354c009a60bcd6d7fc04014e200a1ee9c45c167.patch: -------------------------------------------------------------------------------- 1 | From b354c009a60bcd6d7fc04014e200a1ee9c45c167 Mon Sep 17 00:00:00 2001 2 | From: yaswanthsastry 3 | Date: Mon, 24 Feb 2025 17:07:03 +0530 4 | Subject: [PATCH] Fix CMake build error with latest CMake 4.0 release 5 | 6 | --- 7 | source/CMakeLists.txt | 4 ++-- 8 | 1 file changed, 2 insertions(+), 2 deletions(-) 9 | 10 | diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt 11 | index 37dbe1a87..4f5b3ed82 100755 12 | --- a/source/CMakeLists.txt 13 | +++ b/source/CMakeLists.txt 14 | @@ -7,13 +7,13 @@ if(NOT CMAKE_BUILD_TYPE) 15 | endif() 16 | message(STATUS "cmake version ${CMAKE_VERSION}") 17 | if(POLICY CMP0025) 18 | - cmake_policy(SET CMP0025 OLD) # report Apple's Clang as just Clang 19 | + cmake_policy(SET CMP0025 NEW) # report Apple's Clang as just Clang 20 | endif() 21 | if(POLICY CMP0042) 22 | cmake_policy(SET CMP0042 NEW) # MACOSX_RPATH 23 | endif() 24 | if(POLICY CMP0054) 25 | - cmake_policy(SET CMP0054 OLD) # Only interpret if() arguments as variables or keywords when unquoted 26 | + cmake_policy(SET CMP0054 NEW) # Only interpret if() arguments as variables or keywords when unquoted 27 | endif() 28 | 29 | project (x265) 30 | -- 31 | 2.49.0 32 | 33 | -------------------------------------------------------------------------------- /.ci/yum_deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e -x 3 | 4 | yum -y update 5 | yum install -y epel-release 6 | yum -y install libass libass-devel autoconf automake bzip2 cmake freetype-devel gcc gcc-c++ git libtool make mercurial \ 7 | pkgconfig zlib-devel enca-devel fontconfig-devel openssl openssl-devel wget openjpeg openjpeg-devel \ 8 | libpng libpng-devel libtiff libtiff-devel libwebp libwebp-devel dbus-devel dbus ibus-devel ibus libsamplerate-devel \ 9 | libsamplerate libmodplug-devel libmodplug flac-devel flac \ 10 | libjpeg-turbo-devel libjpeg-turbo pulseaudio pulseaudio-libs-devel alsa-lib alsa-lib-devel ca-certificates perl-devel \ 11 | perl perl-IPC-Cmd patch 12 | -------------------------------------------------------------------------------- /.github/workflows/pythonapp.yml: -------------------------------------------------------------------------------- 1 | name: Python application 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | FFMPEG_VERSION: "6.0" # https://ffmpeg.org/releases/ 7 | SDL_VERSION: "2.26.4" # https://github.com/libsdl-org/SDL/releases 8 | SDL_MIXER_VERSION: "2.6.3" # https://github.com/libsdl-org/SDL_mixer/releases 9 | USE_SDL2_MIXER: "1" 10 | MACOSX_DEPLOYMENT_TARGET: "10.13" 11 | MACOSX_DEPLOYMENT_TARGET_ARM: "11.0" 12 | 13 | jobs: 14 | windows_wheels_tests: 15 | runs-on: windows-latest 16 | env: 17 | FF_BUILD_DIR: ~/ff_deps 18 | SDL_ROOT: ~/ff_deps/SDL2 19 | FFMPEG_ROOT: ~/ff_deps/ffmpeg 20 | strategy: 21 | matrix: 22 | python: [ '3.9', '3.10', '3.11', '3.12', '3.13'] 23 | steps: 24 | - uses: actions/checkout@v4.2.2 25 | - name: Set up Python ${{ matrix.python }} 26 | uses: actions/setup-python@v5.4.0 27 | with: 28 | python-version: ${{ matrix.python }} 29 | - name: Get dependencies 30 | run: | 31 | mkdir "$env:FF_BUILD_DIR" 32 | cd "$env:FF_BUILD_DIR" 33 | 34 | curl -sLO "https://github.com/GyanD/codexffmpeg/releases/download/$env:FFMPEG_VERSION/ffmpeg-$env:FFMPEG_VERSION-full_build-shared.zip" 35 | 7z x "ffmpeg-$env:FFMPEG_VERSION-full_build-shared.zip" 36 | ren "ffmpeg-$env:FFMPEG_VERSION-full_build-shared" ffmpeg 37 | 38 | curl -sLO "https://github.com/libsdl-org/SDL/releases/download/release-$env:SDL_VERSION/SDL2-devel-$env:SDL_VERSION-VC.zip" 39 | 7z x "SDL2-devel-$env:SDL_VERSION-VC.zip" 40 | ren "SDL2-$env:SDL_VERSION" SDL2 41 | curl -sLO "https://github.com/libsdl-org/SDL_mixer/releases/download/release-$env:SDL_MIXER_VERSION/SDL2_mixer-devel-$env:SDL_MIXER_VERSION-VC.zip" 42 | 7z x "SDL2_mixer-devel-$env:SDL_MIXER_VERSION-VC.zip" 43 | 44 | mkdir "SDL2\bin" 45 | mkdir "SDL2\include\SDL2" 46 | 47 | Copy-Item "SDL2\COPYING.txt" -destination "SDL2\bin" 48 | Copy-Item "SDL2\README-SDL.txt" -destination "SDL2\bin" 49 | 50 | Copy-Item "SDL2\lib\x64\*.dll" -destination "SDL2\bin" -Recurse -Force 51 | Copy-Item "SDL2\lib\x64\*.lib" -destination "SDL2\lib" -Recurse -Force 52 | 53 | Copy-Item "SDL2_mixer-$env:SDL_MIXER_VERSION\lib\x64\*.dll" -destination "SDL2\bin" -Recurse -Force 54 | Copy-Item "SDL2_mixer-$env:SDL_MIXER_VERSION\lib\x64\*.lib" -destination "SDL2\lib" -Recurse -Force 55 | Copy-Item "SDL2_mixer-$env:SDL_MIXER_VERSION\include\*" -destination "SDL2\include" -Recurse -Force 56 | 57 | Copy-Item "SDL2\include\*.h" -destination "SDL2\include\SDL2" -Recurse -Force 58 | 59 | echo "Dependency paths are:" 60 | ls $env:SDL_ROOT 61 | ls $env:FFMPEG_ROOT 62 | - name: Install pip deps 63 | run: | 64 | python -m pip install --upgrade pip virtualenv wheel setuptools cython~=3.0.11 pytest 65 | - name: Make sdist 66 | if: matrix.python == '3.13' 67 | run: python setup.py sdist --formats=gztar 68 | - name: Make wheel 69 | run: | 70 | $env:SDL_ROOT=(get-item $env:SDL_ROOT).FullName 71 | $env:FFMPEG_ROOT=(get-item $env:FFMPEG_ROOT).FullName 72 | python setup.py bdist_wheel 73 | - name: Upload wheel 74 | uses: actions/upload-artifact@v4.6.2 75 | with: 76 | name: py_wheel-win-${{ matrix.python }} 77 | path: dist 78 | - name: Upload to GitHub Release 79 | uses: softprops/action-gh-release@v2.2.1 80 | if: startsWith(github.ref, 'refs/tags/') 81 | env: 82 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 83 | with: 84 | files: dist/* 85 | - name: Publish to PyPI 86 | if: startsWith(github.ref, 'refs/tags/') 87 | env: 88 | TWINE_USERNAME: "__token__" 89 | TWINE_PASSWORD: ${{ secrets.pypi_password }} 90 | run: | 91 | python -m pip install twine 92 | twine upload dist/* 93 | - name: Test 94 | run: | 95 | # see https://social.msdn.microsoft.com/Forums/security/en-US/0c13bd1a-388f-48cf-a190-7259d39a080f/ffmpeg-doesnt-work-from-inside-a-container-but-works-on-the-host?forum=windowscontainers 96 | # https://trac.ffmpeg.org/ticket/6875, https://stackoverflow.com/questions/46147012/opencv-import-failed-in-windows-container-on-windows-server-2016 97 | # and https://social.msdn.microsoft.com/Forums/en-US/a95032d2-c469-494a-b3f9-521b1389a6c9/cant-use-opencvpython-package-in-windows-container-windows-server-2016-standard?forum=windowscontainers 98 | # for the reason we need to manually copy some missing dlls to the PATH 99 | Invoke-WebRequest "https://github.com/matham/ffpyplayer/releases/download/v4.1.0/ffmpeg_win_dll_container_deps.zip" -OutFile "ffmpeg_win_dll_container_deps.zip" 100 | 7z x "ffmpeg_win_dll_container_deps.zip" 101 | $env:PATH="$env:PATH;$env:GITHUB_WORKSPACE\ffmpeg_win_dll_container_deps\x64" 102 | ls "$env:GITHUB_WORKSPACE\ffmpeg_win_dll_container_deps\x64" 103 | 104 | $dist_path=(get-item dist).FullName 105 | $root=(get-item .).FullName 106 | $env:FFPYPLAYER_TEST_DIRS="$root\ffpyplayer\tests;$root\examples" 107 | cd ~/ 108 | 109 | python -m pip install --no-index --find-links=$dist_path ffpyplayer 110 | $name = python -c "import ffpyplayer, os.path;print(os.path.dirname(ffpyplayer.__file__))" 111 | echo $name 112 | # powershell interprets writing to stderr as an error, so only raise error if the return code is none-zero 113 | try { 114 | pytest "$name\tests" 115 | } catch { 116 | if ($LastExitCode -ne 0) { 117 | throw $_ 118 | } else { 119 | echo $_ 120 | } 121 | } 122 | 123 | linux_test_src: 124 | strategy: 125 | matrix: 126 | include: 127 | - os: ubuntu-latest 128 | - os: ubuntu-24.04-arm 129 | runs-on: ${{ matrix.os }} 130 | needs: windows_wheels_tests 131 | steps: 132 | - uses: actions/checkout@v4.2.2 133 | - name: Set up Python 3.x 134 | uses: actions/setup-python@v5.4.0 135 | with: 136 | python-version: 3.x 137 | - uses: actions/download-artifact@v4.2.1 138 | with: 139 | pattern: py_wheel-* 140 | merge-multiple: true 141 | path: dist 142 | - name: Install 143 | run: | 144 | sudo apt update 145 | sudo apt install -y ffmpeg libavcodec-dev libavdevice-dev libavfilter-dev libavformat-dev 146 | sudo apt install -y libavutil-dev libswscale-dev libswresample-dev libpostproc-dev libsdl2-dev libsdl2-2.0-0 147 | sudo apt install -y libsdl2-mixer-2.0-0 libsdl2-mixer-dev python3-dev python3 148 | python3 -m pip install pytest 149 | 150 | root=`pwd` 151 | cd ~/ 152 | python3 -m pip install `ls $root/dist/ffpyplayer*.tar.gz` 153 | - name: Test 154 | run: | 155 | root=`pwd` 156 | export FFPYPLAYER_TEST_DIRS="$root/ffpyplayer/tests:$root/examples" 157 | cd ~/ 158 | 159 | name=`python3 -c "import ffpyplayer, os.path;print(os.path.dirname(ffpyplayer.__file__))"` 160 | echo $name 161 | pytest "$name/tests" 162 | 163 | linux_test_wheel: 164 | strategy: 165 | matrix: 166 | include: 167 | - os: ubuntu-latest 168 | - os: ubuntu-24.04-arm 169 | runs-on: ${{ matrix.os }} 170 | needs: linux_wheels 171 | steps: 172 | - uses: actions/checkout@v4.2.2 173 | - name: Set up Python 3.x 174 | uses: actions/setup-python@v5.4.0 175 | with: 176 | python-version: 3.x 177 | - uses: actions/download-artifact@v4.2.1 178 | with: 179 | pattern: py_wheel-* 180 | merge-multiple: true 181 | path: dist 182 | - name: Install 183 | run: | 184 | python3 -m pip install --upgrade pip pytest 185 | root=`pwd` 186 | cd ~/ 187 | python3 -m pip install --no-index --find-links=$root/dist ffpyplayer 188 | - name: Test 189 | run: | 190 | root=`pwd` 191 | export FFPYPLAYER_TEST_DIRS="$root/ffpyplayer/tests:$root/examples" 192 | cd ~/ 193 | 194 | name=`python3 -c "import ffpyplayer, os.path;print(os.path.dirname(ffpyplayer.__file__))"` 195 | echo $name 196 | pytest "$name/tests" 197 | 198 | linux_wheels: 199 | env: 200 | CIBW_ENVIRONMENT_LINUX: "USE_SDL2_MIXER=0 PKG_CONFIG_PATH=$HOME/ffmpeg_build/lib/pkgconfig:$HOME/ffmpeg_build/lib64/pkgconfig LD_LIBRARY_PATH=$HOME/ffmpeg_build/lib:$HOME/ffmpeg_build/lib64:$LD_LIBRARY_PATH" 201 | CIBW_BUILD_VERBOSITY: 3 202 | CIBW_BUILD: ${{ matrix.cibw_build }} 203 | CIBW_ARCHS: ${{ matrix.cibw_archs }} 204 | CIBW_BEFORE_ALL_LINUX: > 205 | cp -r `pwd`/ffmpeg_build $HOME/ffmpeg_build && 206 | source .ci/yum_deps.sh 207 | runs-on: ${{ matrix.os }} 208 | strategy: 209 | matrix: 210 | include: 211 | - os: ubuntu-latest 212 | cibw_archs: 'x86_64' 213 | cibw_build: 'cp39-manylinux_x86_64 cp310-manylinux_x86_64 cp311-manylinux_x86_64 cp312-manylinux_x86_64 cp313-manylinux_x86_64' 214 | - os: ubuntu-24.04-arm 215 | cibw_archs: 'aarch64' 216 | cibw_build: 'cp39-manylinux_aarch64 cp310-manylinux_aarch64 cp311-manylinux_aarch64 cp312-manylinux_aarch64 cp313-manylinux_aarch64' 217 | steps: 218 | - uses: actions/checkout@v4.2.2 219 | - name: Set up Python 3.x 220 | uses: actions/setup-python@v5.4.0 221 | with: 222 | python-version: 3.x 223 | - uses: actions/cache@v4.2.3 224 | id: deps-cache 225 | with: 226 | path: ffmpeg_build 227 | key: ${{ runner.os }}-${{ matrix.cibw_archs }}-deps-cache-${{ hashFiles('**/build-wheels.sh') }}-${{ hashFiles('**/yum_deps.sh') }} 228 | - name: Build dependencies 229 | if: ${{ steps.deps-cache.outputs.cache-hit != 'true' }} 230 | run: | 231 | mkdir dist 232 | docker run --rm -v `pwd`:/io:rw quay.io/pypa/manylinux2014_${{ matrix.cibw_archs }} /io/.ci/build-wheels.sh 233 | - name: Install cibuildwheel 234 | run: | 235 | python -m pip install cibuildwheel~=2.23.3 236 | - name: Make wheels 237 | run: | 238 | python -m cibuildwheel --output-dir dist 239 | - name: Upload wheel 240 | uses: actions/upload-artifact@v4.6.2 241 | with: 242 | name: py_wheel-linux-${{ matrix.os }}-${{ matrix.cibw_archs }} 243 | path: dist 244 | - name: Upload to GitHub Release 245 | uses: softprops/action-gh-release@v2.2.1 246 | if: startsWith(github.ref, 'refs/tags/') 247 | env: 248 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 249 | with: 250 | files: dist/* 251 | - name: Publish to PyPI 252 | if: startsWith(github.ref, 'refs/tags/') 253 | env: 254 | TWINE_USERNAME: "__token__" 255 | TWINE_PASSWORD: ${{ secrets.pypi_password }} 256 | run: | 257 | python -m pip install twine 258 | twine upload dist/* 259 | 260 | osx_wheels_create: 261 | runs-on: macos-13 262 | env: 263 | USE_SDL2_MIXER: 0 264 | FFMPEG_BUILD_PATH: "ffmpeg_build" 265 | CIBW_BUILD_VERBOSITY: 3 266 | CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-* cp313-*" 267 | CIBW_ARCHS_MACOS: ${{ matrix.arch }} 268 | CIBW_REPAIR_WHEEL_COMMAND_MACOS: > 269 | DYLD_FALLBACK_LIBRARY_PATH=$REPAIR_LIBRARY_PATH delocate-listdeps {wheel} && 270 | DYLD_FALLBACK_LIBRARY_PATH=$REPAIR_LIBRARY_PATH delocate-wheel --require-archs {delocate_archs} -w {dest_dir} {wheel} 271 | strategy: 272 | matrix: 273 | arch: [ "x86_64", "arm64" ] 274 | steps: 275 | - uses: actions/checkout@v4.2.2 276 | - name: Set up Python 277 | uses: actions/setup-python@v5.4.0 278 | with: 279 | python-version: 3.x 280 | 281 | - name: Cache ffmpeg 282 | id: cache-ffmpeg 283 | uses: actions/cache@v4.2.3 284 | with: 285 | path: ~/${{ env.FFMPEG_BUILD_PATH }}_${{ matrix.arch }} 286 | key: ${{ runner.os }}-ffmpeg-${{ matrix.arch }}-${{ env.MACOSX_DEPLOYMENT_TARGET }}-${{ env.MACOSX_DEPLOYMENT_TARGET_ARM }}-${{ hashFiles('.ci/build_wheels_osx.sh') }} 287 | - name: Build FFmpeg 288 | if: steps.cache-ffmpeg.outputs.cache-hit != 'true' 289 | run: bash .ci/build_wheels_osx.sh "${{ matrix.arch }}" 290 | 291 | - name: Install cibuildwheel 292 | run: | 293 | python -m pip install cibuildwheel~=2.23.3 294 | - name: Build wheels 295 | run: | 296 | export REPAIR_LIBRARY_PATH="$HOME/${{ env.FFMPEG_BUILD_PATH }}_${{ matrix.arch }}/lib" 297 | export PKG_CONFIG_PATH="$HOME/${{ env.FFMPEG_BUILD_PATH }}_${{ matrix.arch }}/lib/pkgconfig:$PKG_CONFIG_PATH" 298 | python -m cibuildwheel --output-dir dist 299 | 300 | - name: Upload wheel 301 | uses: actions/upload-artifact@v4.6.2 302 | with: 303 | name: py_wheel-osx-${{ matrix.arch }} 304 | path: dist 305 | 306 | osx_wheels_fuse_test_upload: 307 | runs-on: macos-13 308 | needs: osx_wheels_create 309 | steps: 310 | - uses: actions/checkout@v4.2.2 311 | - name: Set up Python 312 | uses: actions/setup-python@v5.4.0 313 | with: 314 | python-version: 3.x 315 | 316 | - uses: actions/download-artifact@v4.2.1 317 | with: 318 | pattern: py_wheel-* 319 | merge-multiple: true 320 | path: dist 321 | 322 | - name: Fuse FFmpeg arm64/x86 323 | run: | 324 | pip install delocate 325 | cd dist 326 | bash ../.ci/merge_osx_deps.sh 327 | 328 | - name: Upload wheel 329 | uses: actions/upload-artifact@v4.6.2 330 | with: 331 | name: py_wheel-osx-fused 332 | path: dist 333 | 334 | - name: Upload to GitHub Release 335 | uses: softprops/action-gh-release@v2.2.1 336 | if: startsWith(github.ref, 'refs/tags/') 337 | env: 338 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 339 | with: 340 | files: dist/* 341 | - name: Publish to PyPI 342 | if: startsWith(github.ref, 'refs/tags/') 343 | env: 344 | TWINE_USERNAME: "__token__" 345 | TWINE_PASSWORD: ${{ secrets.pypi_password }} 346 | run: | 347 | python -m pip install twine 348 | twine upload dist/* 349 | - name: Test 350 | run: | 351 | root=`pwd` 352 | export FFPYPLAYER_TEST_DIRS="$root/ffpyplayer/tests:$root/examples" 353 | cd ~/ 354 | 355 | python -m pip install --upgrade pip virtualenv wheel setuptools pytest 356 | python -m pip install --no-index --find-links=$root/dist ffpyplayer 357 | name=`python -c "import ffpyplayer, os.path;print(os.path.dirname(ffpyplayer.__file__))"` 358 | echo $name 359 | pytest "$name/tests" 360 | 361 | docs: 362 | runs-on: ubuntu-latest 363 | steps: 364 | - uses: actions/checkout@v4.2.2 365 | - name: Set up Python 3.x 366 | uses: actions/setup-python@v5.4.0 367 | with: 368 | python-version: 3.x 369 | - name: Install 370 | run: | 371 | sudo apt update 372 | sudo apt install ffmpeg libavcodec-dev libavdevice-dev libavfilter-dev libavformat-dev 373 | sudo apt install libavutil-dev libswscale-dev libswresample-dev libpostproc-dev libsdl2-dev libsdl2-2.0-0 374 | sudo apt install libsdl2-mixer-2.0-0 libsdl2-mixer-dev python3-dev 375 | 376 | python -m pip install --upgrade pip virtualenv wheel setuptools sphinx sphinx_rtd_theme 377 | python -m pip install -e . 378 | - name: Generate docs 379 | run: | 380 | cd doc 381 | make html 382 | - name: gh-pages upload 383 | if: github.event_name == 'push' && github.ref == 'refs/heads/master' 384 | env: 385 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 386 | run: | 387 | cp -r doc/build/html ~/docs_temp 388 | 389 | git config --global user.email "moiein2000@gmail.com" 390 | git config --global user.name "Matthew Einhorn" 391 | git remote rm origin || true 392 | git remote add origin "https://x-access-token:${GITHUB_TOKEN}@github.com/matham/ffpyplayer.git" 393 | 394 | git checkout --orphan gh-pages 395 | cp -r .git ~/docs_git 396 | cd .. 397 | rm -rf ffpyplayer 398 | mkdir ffpyplayer 399 | cd ffpyplayer 400 | cp -r ~/docs_git .git 401 | cp -r ~/docs_temp/* . 402 | touch .nojekyll 403 | 404 | git add . 405 | git commit -a -m "Docs for git-$GITHUB_SHA" 406 | git push origin gh-pages -f 407 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | *.pyd 3 | *.pyc 4 | ffpyplayer/*.c 5 | ffpyplayer/*.html 6 | ffpyplayer/player/*.c 7 | ffpyplayer/includes/ffconfig.h 8 | ffpyplayer/includes/ffconfig.pxi 9 | *egg-info 10 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTHON = python 2 | 3 | .PHONY: build force test html 4 | 5 | build: 6 | $(PYTHON) setup.py build_ext --inplace 7 | 8 | force: 9 | $(PYTHON) setup.py build_ext --inplace -f 10 | 11 | test: 12 | $(PYTHON) -m pytest ffpyplayer/tests 13 | 14 | html: 15 | @cd doc && make html 16 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | FFPyPlayer is a python binding for the FFmpeg library for playing and writing 2 | media files. 3 | 4 | For more information: https://matham.github.io/ffpyplayer/index.html 5 | 6 | To install: https://matham.github.io/ffpyplayer/installation.html 7 | 8 | .. image:: https://travis-ci.org/matham/ffpyplayer.svg?branch=master 9 | :target: https://travis-ci.org/matham/ffpyplayer 10 | :alt: TravisCI status 11 | 12 | .. image:: https://ci.appveyor.com/api/projects/status/nfl6tyiwks26ngyu/branch/master?svg=true 13 | :target: https://ci.appveyor.com/project/matham/ffpyplayer/branch/master 14 | :alt: Appveyor status 15 | 16 | .. image:: https://img.shields.io/pypi/pyversions/ffpyplayer.svg 17 | :target: https://pypi.python.org/pypi/ffpyplayer/ 18 | :alt: Supported Python versions 19 | 20 | .. image:: https://img.shields.io/pypi/v/ffpyplayer.svg 21 | :target: https://pypi.python.org/pypi/ffpyplayer/ 22 | :alt: Latest Version on PyPI 23 | 24 | .. warning:: 25 | 26 | Although the ffpyplayer source code is licensed under the LGPL, the ffpyplayer wheels 27 | for Windows and linux on PYPI are distributed under the GPL because the included FFmpeg binaries 28 | were compiled with GPL options. 29 | 30 | If you want to use it under the LGPL you need to compile FFmpeg yourself with the correct options. 31 | 32 | Similarly, the wheels bundle openssl for online camera support. However, releases are not made 33 | for every openssl release, so it is recommended that you compile ffpyplayer yourself if security 34 | is a issue. 35 | 36 | Usage example 37 | ------------- 38 | 39 | Playing a file: 40 | 41 | .. code-block:: python 42 | 43 | >>> from ffpyplayer.player import MediaPlayer 44 | >>> import time 45 | 46 | >>> player = MediaPlayer(filename) 47 | >>> val = '' 48 | >>> while val != 'eof': 49 | ... frame, val = player.get_frame() 50 | ... if val != 'eof' and frame is not None: 51 | ... img, t = frame 52 | ... # display img 53 | 54 | Writing a video file: 55 | 56 | .. code-block:: python 57 | 58 | >>> from ffpyplayer.writer import MediaWriter 59 | >>> from ffpyplayer.pic import Image 60 | 61 | >>> w, h = 640, 480 62 | >>> # write at 5 fps. 63 | >>> out_opts = {'pix_fmt_in':'rgb24', 'width_in':w, 'height_in':h, 64 | ... 'codec':'rawvideo', 'frame_rate':(5, 1)} 65 | >>> writer = MediaWriter('output.avi', [out_opts]) 66 | 67 | >>> # Construct image 68 | >>> size = w * h * 3 69 | >>> buf = bytearray([int(x * 255 / size) for x in range(size)]) 70 | >>> img = Image(plane_buffers=[buf], pix_fmt='rgb24', size=(w, h)) 71 | 72 | >>> for i in range(20): 73 | ... writer.write_frame(img=img, pts=i / 5., stream=0) 74 | 75 | Converting images: 76 | 77 | .. code-block:: python 78 | 79 | >>> from ffpyplayer.pic import Image, SWScale 80 | >>> w, h = 500, 100 81 | >>> size = w * h * 3 82 | >>> buf = bytearray([int(x * 255 / size) for x in range(size)]) 83 | 84 | >>> img = Image(plane_buffers=[buf], pix_fmt='rgb24', size=(w, h)) 85 | >>> sws = SWScale(w, h, img.get_pixel_format(), ofmt='yuv420p') 86 | 87 | >>> img2 = sws.scale(img) 88 | >>> img2.get_pixel_format() 89 | 'yuv420p' 90 | >>> planes = img2.to_bytearray() 91 | >>> map(len, planes) 92 | [50000, 12500, 12500, 0] 93 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | @cd .. && python setup.py build_ext --inplace && cd doc 54 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 55 | @echo 56 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 57 | 58 | dirhtml: 59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 60 | @echo 61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 62 | 63 | singlehtml: 64 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 65 | @echo 66 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 67 | 68 | pickle: 69 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 70 | @echo 71 | @echo "Build finished; now you can process the pickle files." 72 | 73 | json: 74 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 75 | @echo 76 | @echo "Build finished; now you can process the JSON files." 77 | 78 | htmlhelp: 79 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 80 | @echo 81 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 82 | ".hhp project file in $(BUILDDIR)/htmlhelp." 83 | 84 | qthelp: 85 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 86 | @echo 87 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 88 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 89 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/FFPyPlayer.qhcp" 90 | @echo "To view the help file:" 91 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/FFPyPlayer.qhc" 92 | 93 | devhelp: 94 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 95 | @echo 96 | @echo "Build finished." 97 | @echo "To view the help file:" 98 | @echo "# mkdir -p $$HOME/.local/share/devhelp/FFPyPlayer" 99 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/FFPyPlayer" 100 | @echo "# devhelp" 101 | 102 | epub: 103 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 104 | @echo 105 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 106 | 107 | latex: 108 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 109 | @echo 110 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 111 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 112 | "(use \`make latexpdf' here to do that automatically)." 113 | 114 | latexpdf: 115 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 116 | @echo "Running LaTeX files through pdflatex..." 117 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 118 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 119 | 120 | latexpdfja: 121 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 122 | @echo "Running LaTeX files through platex and dvipdfmx..." 123 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 124 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 125 | 126 | text: 127 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 128 | @echo 129 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 130 | 131 | man: 132 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 133 | @echo 134 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 135 | 136 | texinfo: 137 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 138 | @echo 139 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 140 | @echo "Run \`make' in that directory to run these through makeinfo" \ 141 | "(use \`make info' here to do that automatically)." 142 | 143 | info: 144 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 145 | @echo "Running Texinfo files through makeinfo..." 146 | make -C $(BUILDDIR)/texinfo info 147 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 148 | 149 | gettext: 150 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 151 | @echo 152 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 153 | 154 | changes: 155 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 156 | @echo 157 | @echo "The overview file is in $(BUILDDIR)/changes." 158 | 159 | linkcheck: 160 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 161 | @echo 162 | @echo "Link check complete; look for any errors in the above output " \ 163 | "or in $(BUILDDIR)/linkcheck/output.txt." 164 | 165 | doctest: 166 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 167 | @echo "Testing of doctests in the sources finished, look at the " \ 168 | "results in $(BUILDDIR)/doctest/output.txt." 169 | 170 | xml: 171 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 172 | @echo 173 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 174 | 175 | pseudoxml: 176 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 177 | @echo 178 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 179 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | cd .. & python setup.py build_ext --inplace & cd doc 65 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 66 | if errorlevel 1 exit /b 1 67 | echo. 68 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 69 | goto end 70 | ) 71 | 72 | if "%1" == "dirhtml" ( 73 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 74 | if errorlevel 1 exit /b 1 75 | echo. 76 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 77 | goto end 78 | ) 79 | 80 | if "%1" == "singlehtml" ( 81 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 82 | if errorlevel 1 exit /b 1 83 | echo. 84 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 85 | goto end 86 | ) 87 | 88 | if "%1" == "pickle" ( 89 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 90 | if errorlevel 1 exit /b 1 91 | echo. 92 | echo.Build finished; now you can process the pickle files. 93 | goto end 94 | ) 95 | 96 | if "%1" == "json" ( 97 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can process the JSON files. 101 | goto end 102 | ) 103 | 104 | if "%1" == "htmlhelp" ( 105 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 106 | if errorlevel 1 exit /b 1 107 | echo. 108 | echo.Build finished; now you can run HTML Help Workshop with the ^ 109 | .hhp project file in %BUILDDIR%/htmlhelp. 110 | goto end 111 | ) 112 | 113 | if "%1" == "qthelp" ( 114 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 115 | if errorlevel 1 exit /b 1 116 | echo. 117 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 118 | .qhcp project file in %BUILDDIR%/qthelp, like this: 119 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\FFPyPlayer.qhcp 120 | echo.To view the help file: 121 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\FFPyPlayer.ghc 122 | goto end 123 | ) 124 | 125 | if "%1" == "devhelp" ( 126 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 127 | if errorlevel 1 exit /b 1 128 | echo. 129 | echo.Build finished. 130 | goto end 131 | ) 132 | 133 | if "%1" == "epub" ( 134 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 135 | if errorlevel 1 exit /b 1 136 | echo. 137 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 138 | goto end 139 | ) 140 | 141 | if "%1" == "latex" ( 142 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 143 | if errorlevel 1 exit /b 1 144 | echo. 145 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 146 | goto end 147 | ) 148 | 149 | if "%1" == "latexpdf" ( 150 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 151 | cd %BUILDDIR%/latex 152 | make all-pdf 153 | cd %BUILDDIR%/.. 154 | echo. 155 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 156 | goto end 157 | ) 158 | 159 | if "%1" == "latexpdfja" ( 160 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 161 | cd %BUILDDIR%/latex 162 | make all-pdf-ja 163 | cd %BUILDDIR%/.. 164 | echo. 165 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 166 | goto end 167 | ) 168 | 169 | if "%1" == "text" ( 170 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 171 | if errorlevel 1 exit /b 1 172 | echo. 173 | echo.Build finished. The text files are in %BUILDDIR%/text. 174 | goto end 175 | ) 176 | 177 | if "%1" == "man" ( 178 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 179 | if errorlevel 1 exit /b 1 180 | echo. 181 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 182 | goto end 183 | ) 184 | 185 | if "%1" == "texinfo" ( 186 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 187 | if errorlevel 1 exit /b 1 188 | echo. 189 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 190 | goto end 191 | ) 192 | 193 | if "%1" == "gettext" ( 194 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 195 | if errorlevel 1 exit /b 1 196 | echo. 197 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 198 | goto end 199 | ) 200 | 201 | if "%1" == "changes" ( 202 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 203 | if errorlevel 1 exit /b 1 204 | echo. 205 | echo.The overview file is in %BUILDDIR%/changes. 206 | goto end 207 | ) 208 | 209 | if "%1" == "linkcheck" ( 210 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 211 | if errorlevel 1 exit /b 1 212 | echo. 213 | echo.Link check complete; look for any errors in the above output ^ 214 | or in %BUILDDIR%/linkcheck/output.txt. 215 | goto end 216 | ) 217 | 218 | if "%1" == "doctest" ( 219 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 220 | if errorlevel 1 exit /b 1 221 | echo. 222 | echo.Testing of doctests in the sources finished, look at the ^ 223 | results in %BUILDDIR%/doctest/output.txt. 224 | goto end 225 | ) 226 | 227 | if "%1" == "xml" ( 228 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 229 | if errorlevel 1 exit /b 1 230 | echo. 231 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 232 | goto end 233 | ) 234 | 235 | if "%1" == "pseudoxml" ( 236 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 237 | if errorlevel 1 exit /b 1 238 | echo. 239 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 240 | goto end 241 | ) 242 | 243 | :end 244 | -------------------------------------------------------------------------------- /doc/source/api.rst: -------------------------------------------------------------------------------- 1 | 2 | #################### 3 | The FFPyPlayer API 4 | #################### 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | player.rst 10 | writer.rst 11 | pic.rst 12 | tools.rst 13 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # http://www.sphinx-doc.org/en/master/config 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sphinx_rtd_theme 15 | 16 | 17 | # -- Project information ----------------------------------------------------- 18 | 19 | project = 'FFPyPlayer' 20 | copyright = '2013, Matthew Einhorn' 21 | author = 'Matthew Einhorn' 22 | 23 | 24 | # -- General configuration --------------------------------------------------- 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be 27 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 28 | # ones. 29 | extensions = [ 30 | 'sphinx.ext.autodoc', 31 | 'sphinx.ext.todo', 32 | 'sphinx.ext.coverage', 33 | "sphinx_rtd_theme", 34 | ] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # List of patterns, relative to source directory, that match files and 40 | # directories to ignore when looking for source files. 41 | # This pattern also affects html_static_path and html_extra_path. 42 | exclude_patterns = [] 43 | 44 | 45 | # -- Options for HTML output ------------------------------------------------- 46 | 47 | # The theme to use for HTML and HTML Help pages. See the documentation for 48 | # a list of builtin themes. 49 | # 50 | html_theme = 'sphinx_rtd_theme' 51 | 52 | # Add any paths that contain custom static files (such as style sheets) here, 53 | # relative to this directory. They are copied after the builtin static files, 54 | # so a file named "default.css" will overwrite the builtin "default.css". 55 | html_static_path = ['_static'] 56 | -------------------------------------------------------------------------------- /doc/source/examples.rst: -------------------------------------------------------------------------------- 1 | .. _examples: 2 | 3 | ******** 4 | Examples 5 | ******** 6 | 7 | 8 | Converting Image formats 9 | ------------------------ 10 | 11 | .. code-block:: python 12 | 13 | from ffpyplayer.pic import Image, SWScale 14 | w, h = 500, 100 15 | size = w * h * 3 16 | buf = bytearray([int(x * 255 / size) for x in range(size)]) 17 | 18 | img = Image(plane_buffers=[buf], pix_fmt='rgb24', size=(w, h)) 19 | sws = SWScale(w, h, img.get_pixel_format(), ofmt='yuv420p') 20 | 21 | img2 = sws.scale(img) 22 | img2.get_pixel_format() 23 | 'yuv420p' 24 | planes = img2.to_bytearray() 25 | map(len, planes) 26 | [50000, 12500, 12500, 0] 27 | 28 | .. _dshow-example: 29 | 30 | Playing a webcam with DirectShow on windows 31 | ------------------------------------------- 32 | 33 | One can use :meth:`~ffpyplayer.tools.list_dshow_devices` to get a list of the 34 | devices and their option for playing. For example: 35 | 36 | .. code-block:: python 37 | 38 | # see http://ffmpeg.org/ffmpeg-formats.html#Format-Options for rtbufsize 39 | # lets use the yuv420p, 320x240, 30fps 40 | # 27648000 = 320*240*3 at 30fps, for 4 seconds. 41 | # see http://ffmpeg.org/ffmpeg-devices.html#dshow for video_size, and framerate 42 | lib_opts = {'framerate':'30', 'video_size':'320x240', 43 | 'pixel_format': 'yuv420p', 'rtbufsize':'27648000'} 44 | ff_opts = {'f':'dshow'} 45 | player = MediaPlayer('video=Logitech HD Webcam C525:audio=Microphone (HD Webcam C525)', 46 | ff_opts=ff_opts, lib_opts=lib_opts) 47 | 48 | while 1: 49 | frame, val = player.get_frame() 50 | if val == 'eof': 51 | break 52 | elif frame is None: 53 | time.sleep(0.01) 54 | else: 55 | img, t = frame 56 | print val, t, img.get_pixel_format(), img.get_buffer_size() 57 | time.sleep(val) 58 | 0.0 264107.429 rgb24 (230400, 0, 0, 0) 59 | 0.0 264108.364 rgb24 (230400, 0, 0, 0) 60 | 0.0790016651154 264108.628 rgb24 (230400, 0, 0, 0) 61 | 0.135997533798 264108.764 rgb24 (230400, 0, 0, 0) 62 | 0.274529457092 264108.897 rgb24 (230400, 0, 0, 0) 63 | 0.272421836853 264109.028 rgb24 (230400, 0, 0, 0) 64 | 0.132406949997 264109.164 rgb24 (230400, 0, 0, 0) 65 | ... 66 | 67 | # NOTE, by default the output was rgb24. To keep the output format the 68 | # same as the input, do ff_opts['out_fmt'] = 'yuv420p' 69 | 70 | 71 | Saving an image to disk 72 | ----------------------- 73 | 74 | .. code-block:: python 75 | 76 | from ffpyplayer.pic import Image, SWScale 77 | from ffpyplayer.tools import get_supported_pixfmts 78 | 79 | # create image 80 | w, h = 500, 100 81 | fmt = 'rgb24' 82 | size = w * h * 3 83 | buf = bytearray([int(x * 255 / size) for x in range(size)]) 84 | img = Image(plane_buffers=[buf], pix_fmt=fmt, size=(w, h)) 85 | codec = 'tiff' # we'll encode it using the tiff codec 86 | 87 | # make sure the output codec supports the input pixel format type 88 | # otherwise, convert it to the best pixel format 89 | ofmt = get_supported_pixfmts(codec, fmt)[0] 90 | if ofmt != fmt: 91 | sws = SWScale(w, h, fmt, ofmt=ofmt) 92 | img = sws.scale(img) 93 | fmt = ofmt 94 | 95 | out_opts = {'pix_fmt_in': fmt, 'width_in': w, 'height_in': h, 96 | 'frame_rate': (30, 1), 'codec': codec} 97 | writer = MediaWriter('myfile.tiff', [out_opts]) 98 | writer.write_frame(img=img, pts=0, stream=0) 99 | writer.close() 100 | 101 | # to save the file as a compressed tiff using lzw 102 | writer = MediaWriter('myfile.tiff', [out_opts], lib_opts={'compression_algo': 'lzw'}) 103 | writer.write_frame(img=img, pts=0, stream=0) 104 | writer.close() 105 | 106 | Simple transcoding example 107 | -------------------------- 108 | 109 | .. code-block:: python 110 | 111 | from ffpyplayer.player import MediaPlayer 112 | from ffpyplayer.writer import MediaWriter 113 | import time, weakref 114 | 115 | # only video 116 | ff_opts={'an':True, 'sync':'video'} 117 | player = MediaPlayer(filename, ff_opts=ff_opts) 118 | # wait for size to be initialized (todo: add timeout and check for quitting) 119 | while player.get_metadata()['src_vid_size'] == (0, 0): 120 | time.sleep(0.01) 121 | 122 | frame_size = player.get_metadata()['src_vid_size'] 123 | # use the same size as the inputs 124 | out_opts = {'pix_fmt_in':'rgb24', 'width_in':frame_size[0], 125 | 'height_in':frame_size[1], 'codec':'rawvideo', 126 | 'frame_rate':(30, 1)} 127 | 128 | writer = MediaWriter(filename_out, [out_opts]) 129 | while 1: 130 | frame, val = player.get_frame() 131 | if val == 'eof': 132 | break 133 | elif frame is None: 134 | time.sleep(0.01) 135 | else: 136 | img, t = frame 137 | writer.write_frame(img=img, pts=t, stream=0) 138 | 139 | More complex transcoding example 140 | -------------------------------- 141 | 142 | .. code-block:: python 143 | 144 | from ffpyplayer.player import MediaPlayer 145 | from ffpyplayer.tools import free_frame_ref 146 | from ffpyplayer.writer import MediaWriter 147 | import time, weakref 148 | 149 | # only video, output yuv420p frames 150 | ff_opts={'an':True, 'sync':'video', 'out_fmt':'yuv420p'} 151 | player = MediaPlayer(filename, ff_opts=ff_opts) 152 | # wait for size to be initialized 153 | while player.get_metadata()['src_vid_size'] == (0, 0): 154 | time.sleep(0.01) 155 | 156 | frame_size = player.get_metadata()['src_vid_size'] 157 | # use the half the size for the output as the input 158 | out_opts = {'pix_fmt_in':'yuv420p', 'width_in':frame_size[0], 159 | 'height_in':frame_size[1], 'codec':'rawvideo', 160 | 'frame_rate':(30, 1), 'width_out':frame_size[0] / 2, 161 | 'height_out':frame_size[1] / 2} 162 | 163 | writer = MediaWriter(filename_out, [out_opts]) 164 | while 1: 165 | frame, val = player.get_frame() 166 | if val == 'eof': 167 | break 168 | elif frame is None: 169 | time.sleep(0.01) 170 | else: 171 | img, t = frame 172 | writer.write_frame(img=img, pts=t, stream=0) 173 | 174 | .. _write-simple: 175 | 176 | Writing video to file 177 | --------------------- 178 | 179 | .. code-block:: python 180 | 181 | from ffpyplayer.writer import MediaWriter 182 | from ffpyplayer.pic import Image 183 | 184 | w, h = 640, 480 185 | # write at 5 fps. 186 | out_opts = {'pix_fmt_in':'rgb24', 'width_in':w, 'height_in':h, 'codec':'rawvideo', 187 | 'frame_rate':(5, 1)} 188 | # write using rgb24 frames into a two stream rawvideo file where the output 189 | # is half the input size for both streams. Avi format will be used. 190 | writer = MediaWriter('output.avi', [out_opts] * 2, width_out=w/2, 191 | height_out=h/2) 192 | 193 | # Construct images 194 | size = w * h * 3 195 | buf = bytearray([int(x * 255 / size) for x in range(size)]) 196 | img = Image(plane_buffers=[buf], pix_fmt='rgb24', size=(w, h)) 197 | 198 | buf = bytearray([int((size - x) * 255 / size) for x in range(size)]) 199 | img2 = Image(plane_buffers=[buf], pix_fmt='rgb24', size=(w, h)) 200 | 201 | for i in range(20): 202 | writer.write_frame(img=img, pts=i / 5., stream=0) # stream 1 203 | writer.write_frame(img=img2, pts=i / 5., stream=1) # stream 2 204 | 205 | Or force an output format of avi, even though the filename is .mp4.: 206 | 207 | .. code-block:: python 208 | 209 | writer = MediaWriter('output.mp4', [out_opts] * 2, fmt='avi', 210 | width_out=w/2, height_out=h/2) 211 | 212 | .. _write-h264: 213 | 214 | Compressing video to h264 215 | ------------------------- 216 | 217 | Or writing compressed h264 files (notice the file is now only 5KB, while 218 | the above results in a 10MB file): 219 | 220 | .. code-block:: python 221 | 222 | from ffpyplayer.writer import MediaWriter 223 | from ffpyplayer.tools import get_supported_pixfmts, get_supported_framerates 224 | from ffpyplayer.pic import Image 225 | 226 | # make sure the pixel format and rate are supported. 227 | print get_supported_pixfmts('libx264', 'rgb24') 228 | #['yuv420p', 'yuvj420p', 'yuv422p', 'yuvj422p', 'yuv444p', 'yuvj444p', 'nv12', 'nv16'] 229 | print get_supported_framerates('libx264', (5, 1)) 230 | #[] 231 | w, h = 640, 480 232 | out_opts = {'pix_fmt_in':'rgb24', 'width_in':w, 'height_in':h, 'codec':'libx264', 233 | 'frame_rate':(5, 1)} 234 | 235 | # use the following libx264 compression options 236 | lib_opts = {'preset':'slow', 'crf':'22'} 237 | # set the following metadata (ffmpeg doesn't always support writing metadata) 238 | metadata = {'title':'Singing in the sun', 'author':'Rat', 'genre':'Animal sounds'} 239 | 240 | # write using yuv420p frames into a two stream h264 codec, mp4 file where the output 241 | # is half the input size for both streams. 242 | writer = MediaWriter('output.avi', [out_opts] * 2, fmt='mp4', 243 | width_out=w/2, height_out=h/2, pix_fmt_out='yuv420p', 244 | lib_opts=lib_opts, metadata=metadata) 245 | 246 | # Construct images 247 | size = w * h * 3 248 | buf = bytearray([int(x * 255 / size) for x in range(size)]) 249 | img = Image(plane_buffers=[buf], pix_fmt='rgb24', size=(w, h)) 250 | 251 | buf = bytearray([int((size - x) * 255 / size) for x in range(size)]) 252 | img2 = Image(plane_buffers=[buf], pix_fmt='rgb24', size=(w, h)) 253 | 254 | for i in range(20): 255 | writer.write_frame(img=img, pts=i / 5., stream=0) # stream 1 256 | writer.write_frame(img=img2, pts=i / 5., stream=1) # stream 2 257 | -------------------------------------------------------------------------------- /doc/source/getting_started.rst: -------------------------------------------------------------------------------- 1 | .. _started: 2 | 3 | #################### 4 | Getting Started 5 | #################### 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | installation.rst 11 | examples.rst 12 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. FFPyPlayer documentation master file, created by 2 | sphinx-quickstart on Mon Dec 23 18:07:03 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to FFPyPlayer's documentation! 7 | ====================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | getting_started.rst 15 | api.rst 16 | 17 | * :ref:`genindex` 18 | * :ref:`modindex` 19 | * :ref:`search` 20 | -------------------------------------------------------------------------------- /doc/source/installation.rst: -------------------------------------------------------------------------------- 1 | .. _install: 2 | 3 | ************ 4 | Installation 5 | ************ 6 | 7 | Using binary wheels 8 | ------------------- 9 | 10 | On windows 7+ (64 or 32 bit) and linux (64 bit), ffpyplayer wheels can be installed for 11 | python 3.5+ using:: 12 | 13 | pip install ffpyplayer 14 | 15 | .. warning:: 16 | 17 | Although the ffpyplayer source code is licensed under the LGPL, the ffpyplayer wheels 18 | on PYPI are distributed under the GPL because the FFmpeg binaries 19 | are GPL'd. For LGPL builds you can compile FFmpeg yourself using LGPL options. 20 | 21 | For other OSs or to compile with master see below. 22 | 23 | Compiling 24 | --------- 25 | 26 | Requirements 27 | ============ 28 | 29 | To compile ffpyplayer we need: 30 | 31 | * Cython (``pip install --upgrade cython~=3.0.11``). 32 | * A c compiler e.g. gcc or MSVC. 33 | * SDL2 or SDL1.2 (SDL1.2 is not recommended). See :ref:`compille` for how to get it. 34 | * SDL2_mixer If wanting to play multiple audio files simultaneously (``USE_SDL2_MIXER`` must be set). See :ref:`compille` for how to get it. 35 | * A recent (2.x+, has been tested with 2.8) FFmpeg compiled with ``--enable-shared``. 36 | See :ref:`compille` for how to get it. 37 | 38 | Compiling ffpyplayer 39 | ==================== 40 | 41 | * Download or compile FFMpeg and SDL2 as shown below and set the appropriate environment variables as needed. 42 | * Install Cython with e.g.:: 43 | 44 | pip install --upgrade cython~=3.0.11 45 | 46 | * You can select the FFmpeg libraries to be used by defining values for CONFIG_XXX. 47 | For example, CONFIG_AVFILTER=0 will disable inclusion of the FFmpeg avfilter libraries. 48 | See setup.py for all the available flags. 49 | * To use SDL2_mixer, which is required when multiple audio files are to be played 50 | simultaneously (or even when they are open at the same time) environment variable ``USE_SDL2_MIXER`` 51 | must be set to 1 when compiling. SDL2_mixer binaries and headers must also be available. 52 | * Finally, run:: 53 | 54 | pip install ffpyplayer 55 | 56 | Or to install master, do:: 57 | 58 | pip install https://github.com/matham/ffpyplayer/archive/master.zip 59 | 60 | If you have a local directory with the ffpyplayer source code. To compile, you can run within that directory 61 | * ``make`` on linux, or 62 | * ``python setup.py build_ext --inplace``, or 63 | * ``pip install -e .`` to also properly install it. 64 | 65 | You should now be able to import ffpyplayer with ``import ffpyplayer``. 66 | 67 | .. _compille 68 | 69 | SDL and Compiling FFmpeg 70 | ------------------------ 71 | 72 | To use ffpyplayer, the compiled FFmpeg and SDL shared libraries must be available. Following are 73 | instructions for the various OSs. 74 | 75 | Windows 76 | ======= 77 | 78 | You can get pre-compiled FFmpeg libaries from http://ffmpeg.zeranoe.com/builds/. You need 79 | both the shared (which contains the .a files and headers) and the dev (which contains the dlls) 80 | downloads. 81 | 82 | You can download SDL2 from https://www.libsdl.org/release/. 2.0.4 is the most recent 83 | `version `_. 84 | 85 | You can download SDL2_mixer from https://www.libsdl.org/projects/SDL_mixer/. 2.0.1 is the most recent 86 | `version `_. 87 | 88 | * If there's a root directory containing a ``include`` and ``lib`` directory, each containing the header 89 | and compiled binaries, respectively, then ``FFMPEG_ROOT`` and ``SDL_ROOT`` can be set to these 90 | root directories for ffmpeg and sdl, respectively. Otherwise, 91 | * ``SDL_LIB_DIR`` and ``FFMPEG_LIB_DIR`` should point to a folder which contains the 92 | SDL and FFmpeg compiled shared libraries (*.dll), respectively. 93 | * ``FFMPEG_INCLUDE_DIR`` should point to a directory which contains the FFmpeg header files. 94 | * ``SDL_INCLUDE_DIR`` should point to a directory containg the SDL headers. For SDL2, 95 | this directory contains a SDL2 named directory with all the headers. 96 | 97 | In addition, directories containing the SDL and FFmpeg shared libraries (*.dll) need to be added to the PATH. 98 | 99 | OSX 100 | === 101 | 102 | You can get both FFmpeg and SDL2 using brew. You can install them using:: 103 | 104 | brew update 105 | brew install sdl2 sdl2_mixer ffmpeg 106 | 107 | Otherwise, follow the Linux instructions. 108 | 109 | Linux 110 | ====== 111 | 112 | Ubuntu 18.04 113 | ~~~~~~~~~~~~ 114 | 115 | On Ubuntu 18.04, the following command will install the python, ffmpeg, and sdl2 dependencies:: 116 | 117 | sudo apt install ffmpeg libavcodec-dev libavdevice-dev libavfilter-dev libavformat-dev \ 118 | libavutil-dev libswscale-dev libswresample-dev libpostproc-dev libsdl2-dev libsdl2-2.0-0 \ 119 | libsdl2-mixer-2.0-0 libsdl2-mixer-dev python3-dev 120 | 121 | Other Linux platforms 122 | ~~~~~~~~~~~~~~~~~~~~~~ 123 | 124 | FFMpeg 125 | ^^^^^^^ 126 | 127 | Follow the instructions at https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu to compile FFMpeg. 128 | However, those instructions detail how to build the static version. We need the shared 129 | version. This means that ``--enable-shared`` and ``--extra-cflags="-fPIC"`` need to be added 130 | when compiling FFmpeg **AND** its dependencies. And if present, ``--disable-shared`` or 131 | ``--enable-static`` must be removed. 132 | 133 | Following that guide, ``export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$HOME/ffmpeg_build/lib`` also needs 134 | to be executed for the compiled binaries to be found. 135 | 136 | SDL2 137 | ^^^^^ 138 | 139 | SDL2 can usually be gotten from the package manager, e.g. in Ubuntu 16.04 you can do the following:: 140 | 141 | sudo apt-get update 142 | sudo apt-get -y install libsdl2-dev libsdl2-mixer-dev 143 | 144 | Python Headers 145 | ^^^^^^^^^^^^^^^ 146 | 147 | The Python headers are required for compilation, on Ubuntu you can get it with:: 148 | 149 | sudo apt-get install python3-dev 150 | 151 | For either ffmpeg or sdl2 if manually compiled, ``PKG_CONFIG_PATH`` will need to be set to the path 152 | containing the generated `*.pc` files and ``pkg-config`` will need to be available. *Otherwise,* if 153 | installed to a non-standard location, the paths to the compiled shared libraries and headers will need to be set with 154 | 155 | * If there's a root directory containing a ``include`` and ``lib`` directory, each containing the header 156 | and compiled binaries, respectively, then ``FFMPEG_ROOT`` and ``SDL_ROOT`` can be set to these 157 | root directories for ffmpeg and sdl, respectively. Otherwise, 158 | * ``SDL_LIB_DIR`` and ``FFMPEG_LIB_DIR`` should point to a folder which contains the 159 | SDL and FFmpeg compiled shared libraries (*.so), respectively. 160 | * ``FFMPEG_INCLUDE_DIR`` should point to a directory which contains the FFmpeg header files. 161 | * ``SDL_INCLUDE_DIR`` should point to a directory containg the SDL headers. For SDL2, 162 | this directory contains a SDL2 named directory with all the headers. 163 | 164 | In addition, directories containing the SDL and FFmpeg shared libraries (*.so) need to be added to the PATH. 165 | 166 | You can find a complete minimal example of compiling ffpyplayer on Ubuntu 167 | `here `_. 168 | A more complete example used to build the wheels is 169 | `here `_. 170 | 171 | -------------------------------------------------------------------------------- /doc/source/pic.rst: -------------------------------------------------------------------------------- 1 | .. _pic-api: 2 | 3 | ****** 4 | Images 5 | ****** 6 | 7 | :mod:`ffpyplayer.pic` 8 | ============================= 9 | 10 | .. automodule:: ffpyplayer.pic 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | -------------------------------------------------------------------------------- /doc/source/player.rst: -------------------------------------------------------------------------------- 1 | .. _player-api: 2 | 3 | ****** 4 | Player 5 | ****** 6 | 7 | :mod:`ffpyplayer.player` 8 | ============================= 9 | 10 | .. automodule:: ffpyplayer.player 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | -------------------------------------------------------------------------------- /doc/source/tools.rst: -------------------------------------------------------------------------------- 1 | .. _tools-api: 2 | 3 | ***** 4 | Tools 5 | ***** 6 | 7 | :mod:`ffpyplayer.tools` 8 | ============================= 9 | 10 | .. automodule:: ffpyplayer.tools 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | .. autoattribute:: ffpyplayer.tools.loglevels 16 | 17 | A dictionary with all the available ffmpeg log levels. The keys are the loglevels 18 | and the values are their ffmpeg values. The lower the value, the more important 19 | the log. Note, this is ooposite python where the higher the level the more important 20 | the log. 21 | 22 | .. autoattribute:: ffpyplayer.tools.codecs_enc 23 | 24 | A list of all the codecs available for encoding video. 25 | 26 | .. autoattribute:: ffpyplayer.tools.codecs_dec 27 | 28 | A list of all the codecs available for decoding video and audio. 29 | 30 | .. autoattribute:: ffpyplayer.tools.pix_fmts 31 | 32 | A list of all the pixel formats available to ffmpeg. 33 | 34 | .. autoattribute:: ffpyplayer.tools.formats_in 35 | 36 | A list of all the formats (e.g. file formats) available for reading. 37 | 38 | .. autoattribute:: ffpyplayer.tools.formats_out 39 | 40 | A list of all the formats (e.g. file formats) available for writing. 41 | -------------------------------------------------------------------------------- /doc/source/writer.rst: -------------------------------------------------------------------------------- 1 | .. _writer-api: 2 | 3 | ****** 4 | Writer 5 | ****** 6 | 7 | :mod:`ffpyplayer.writer` 8 | ============================= 9 | 10 | .. automodule:: ffpyplayer.writer 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | -------------------------------------------------------------------------------- /examples/dw11222.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matham/ffpyplayer/3684c387e217c965d2fc5eb6fab4dfba196e619b/examples/dw11222.mp4 -------------------------------------------------------------------------------- /examples/eye.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matham/ffpyplayer/3684c387e217c965d2fc5eb6fab4dfba196e619b/examples/eye.gif -------------------------------------------------------------------------------- /examples/test.py: -------------------------------------------------------------------------------- 1 | ''' 2 | To run, please provide a filename on the command line when running the file. 3 | ''' 4 | 5 | 6 | import kivy 7 | from kivy.base import EventLoop 8 | EventLoop.ensure_window() 9 | from ffpyplayer.player import MediaPlayer 10 | from ffpyplayer.tools import set_log_callback, loglevels 11 | from kivy.clock import Clock 12 | from kivy.graphics.texture import Texture 13 | from kivy.app import App 14 | from kivy.core.window import Window 15 | from kivy.lang import Builder 16 | from kivy.uix.relativelayout import RelativeLayout 17 | from kivy.weakmethod import WeakMethod 18 | import sys 19 | import time 20 | from threading import RLock, Thread 21 | import logging 22 | logging.root.setLevel(logging.DEBUG) 23 | 24 | 25 | Builder.load_string(''' 26 | : 27 | id: rt 28 | image: img 29 | volume: volume 30 | seek: seek 31 | Image: 32 | id: img 33 | size_hint: 0.95, 0.95 34 | pos: 0.05 * rt.width, 0.05 * rt.height 35 | allow_stretch: False 36 | on_size: app.resize() 37 | ProgressBar: 38 | id: seek 39 | size_hint: 0.95, 0.05 40 | pos: 0.05 * rt.width, 0 41 | on_touch_down: app.touch_down(args[1]) 42 | value: 0 43 | Slider: 44 | id: volume 45 | orientation: 'vertical' 46 | size_hint: 0.05, 1. 47 | pos: 0.0, 0.0 48 | step: 0.01 49 | value: 1. 50 | range: 0., 1. 51 | on_value: app.ffplayer and app.ffplayer.set_volume(self.value) 52 | ''') 53 | 54 | class Root(RelativeLayout): 55 | pass 56 | 57 | log_level = 'debug' 58 | logger_func = {'quiet': logging.critical, 'panic': logging.critical, 59 | 'fatal': logging.critical, 'error': logging.error, 60 | 'warning': logging.warning, 'info': logging.info, 61 | 'verbose': logging.debug, 'debug': logging.debug} 62 | 63 | 64 | def log_callback(message, level): 65 | message = message.strip() 66 | if message: 67 | logger_func[level]('ffpyplayer: {}'.format(message)) 68 | 69 | 70 | class PlayerApp(App): 71 | 72 | def __init__(self, **kwargs): 73 | super(PlayerApp, self).__init__(**kwargs) 74 | self.texture = None 75 | self.size = (0, 0) 76 | self.next_frame = None 77 | self._done = False 78 | self._lock = RLock() 79 | self._thread = Thread(target=self._next_frame, name='Next frame') 80 | self._trigger = Clock.create_trigger(self.redraw) 81 | self._force_refresh = False 82 | 83 | def build(self): 84 | self.root = Root() 85 | return self.root 86 | 87 | def on_start(self): 88 | self.callback_ref = WeakMethod(self.callback) 89 | filename = sys.argv[1] 90 | logging.info('ffpyplayer: Playing file "{}"'.format(filename)) 91 | # try ff_opts = {'vf':'edgedetect'} http://ffmpeg.org/ffmpeg-filters.html 92 | ff_opts = {} 93 | self.ffplayer = MediaPlayer(filename, callback=self.callback_ref, 94 | loglevel=log_level, ff_opts=ff_opts) 95 | self._thread.start() 96 | self.keyboard = Window.request_keyboard(None, self.root) 97 | self.keyboard.bind(on_key_down=self.on_keyboard_down) 98 | 99 | def resize(self): 100 | if self.ffplayer: 101 | w, h = self.ffplayer.get_metadata()['src_vid_size'] 102 | if not h: 103 | return 104 | lock = self._lock 105 | lock.acquire() 106 | if self.root.image.width < self.root.image.height * w / float(h): 107 | self.ffplayer.set_size(-1, self.root.image.height) 108 | else: 109 | self.ffplayer.set_size(self.root.image.width, -1) 110 | lock.release() 111 | logging.debug('ffpyplayer: Resized video.') 112 | 113 | def update_pts(self, *args): 114 | if self.ffplayer: 115 | self.root.seek.value = self.ffplayer.get_pts() 116 | 117 | def on_keyboard_down(self, keyboard, keycode, text, modifiers): 118 | if not self.ffplayer: 119 | return False 120 | lock = self._lock 121 | ctrl = 'ctrl' in modifiers 122 | if keycode[1] == 'p' or keycode[1] == 'spacebar': 123 | logging.info('Toggled pause.') 124 | self.ffplayer.toggle_pause() 125 | elif keycode[1] == 'r': 126 | logging.debug('ffpyplayer: Forcing a refresh.') 127 | self._force_refresh = True 128 | elif keycode[1] == 'v': 129 | logging.debug('ffpyplayer: Changing video stream.') 130 | lock.acquire() 131 | self.ffplayer.request_channel('video', 132 | 'close' if ctrl else 'cycle') 133 | lock.release() 134 | Clock.unschedule(self.update_pts) 135 | if ctrl: # need to continue updating pts, since video is disabled. 136 | Clock.schedule_interval(self.update_pts, 0.05) 137 | elif keycode[1] == 'a': 138 | logging.debug('ffpyplayer: Changing audio stream.') 139 | lock.acquire() 140 | self.ffplayer.request_channel('audio', 141 | 'close' if ctrl else 'cycle') 142 | lock.release() 143 | elif keycode[1] == 't': 144 | logging.debug('ffpyplayer: Changing subtitle stream.') 145 | lock.acquire() 146 | self.ffplayer.request_channel('subtitle', 147 | 'close' if ctrl else 'cycle') 148 | lock.release() 149 | elif keycode[1] == 'right': 150 | logging.debug('ffpyplayer: Seeking forward by 10s.') 151 | self.ffplayer.seek(10.) 152 | elif keycode[1] == 'left': 153 | logging.debug('ffpyplayer: Seeking back by 10s.') 154 | self.ffplayer.seek(-10.) 155 | elif keycode[1] == 'up': 156 | logging.debug('ffpyplayer: Increasing volume.') 157 | self.ffplayer.set_volume(self.ffplayer.get_volume() + 0.01) 158 | self.root.volume.value = self.ffplayer.get_volume() 159 | elif keycode[1] == 'down': 160 | logging.debug('ffpyplayer: Decreasing volume.') 161 | self.ffplayer.set_volume(self.ffplayer.get_volume() - 0.01) 162 | self.root.volume.value = self.ffplayer.get_volume() 163 | return True 164 | 165 | def touch_down(self, touch): 166 | if self.root.seek.collide_point(*touch.pos) and self.ffplayer: 167 | pts = ((touch.pos[0] - self.root.volume.width) / 168 | self.root.seek.width * self.ffplayer.get_metadata()['duration']) 169 | logging.debug('ffpyplayer: Seeking to {}.'.format(pts)) 170 | self.ffplayer.seek(pts, relative=False) 171 | self._force_refresh = True 172 | return True 173 | return False 174 | 175 | def callback(self, selector, value): 176 | if self.ffplayer is None: 177 | return 178 | if selector == 'quit': 179 | logging.debug('ffpyplayer: Quitting.') 180 | def close(*args): 181 | self._done = True 182 | self.ffplayer = None 183 | Clock.schedule_once(close, 0) 184 | # called from internal thread, it typically reads forward 185 | elif selector == 'display_sub': 186 | self.display_subtitle(*value) 187 | 188 | def _next_frame(self): 189 | ffplayer = self.ffplayer 190 | sleep = time.sleep 191 | trigger = self._trigger 192 | while not self._done: 193 | force = self._force_refresh 194 | if force: 195 | self._force_refresh = False 196 | frame, val = ffplayer.get_frame(force_refresh=force) 197 | 198 | if val == 'eof': 199 | logging.debug('ffpyplayer: Got eof.') 200 | sleep(1 / 30.) 201 | elif val == 'paused': 202 | logging.debug('ffpyplayer: Got paused.') 203 | sleep(1 / 30.) 204 | else: 205 | if frame: 206 | logging.debug('ffpyplayer: Next frame: {}.'.format(val)) 207 | sleep(val) 208 | self.next_frame = frame 209 | trigger() 210 | else: 211 | val = val if val else (1 / 30.) 212 | logging.debug('ffpyplayer: Schedule next frame check: {}.' 213 | .format(val)) 214 | sleep(val) 215 | 216 | def redraw(self, dt=0, force_refresh=False): 217 | if not self.ffplayer: 218 | return 219 | if self.next_frame: 220 | img, pts = self.next_frame 221 | if img.get_size() != self.size or self.texture is None: 222 | self.root.image.canvas.remove_group(str(self)+'_display') 223 | self.texture = Texture.create(size=img.get_size(), 224 | colorfmt='rgb') 225 | # by adding 'vf':'vflip' to the player initialization ffmpeg 226 | # will do the flipping 227 | self.texture.flip_vertical() 228 | self.texture.add_reload_observer(self.reload_buffer) 229 | self.size = img.get_size() 230 | logging.debug('ffpyplayer: Creating new image texture of ' 231 | 'size: {}.'.format(self.size)) 232 | self.texture.blit_buffer(img.to_memoryview()[0]) 233 | self.root.image.texture = None 234 | self.root.image.texture = self.texture 235 | self.root.seek.value = pts 236 | logging.debug('ffpyplayer: Blitted new frame with time: {}.' 237 | .format(pts)) 238 | 239 | if self.root.seek.value: 240 | self.root.seek.max = self.ffplayer.get_metadata()['duration'] 241 | 242 | def display_subtitle(self, text, fmt, pts, t_start, t_end): 243 | pass # fmt is text (unformatted), or ass (formatted subs) 244 | 245 | def reload_buffer(self, *args): 246 | logging.debug('ffpyplayer: Reloading buffer.') 247 | frame = self.next_frame 248 | if not frame: 249 | return 250 | self.texture.blit_buffer(frame[0].to_memoryview()[0], colorfmt='rgb', 251 | bufferfmt='ubyte') 252 | 253 | if __name__ == '__main__': 254 | set_log_callback(log_callback) 255 | a = PlayerApp() 256 | a.run() 257 | # because MediaPlayer runs non-daemon threads, when the main thread exists 258 | # it'll get stuck waiting for those threads to close, so we manually 259 | # have to delete these threads by deleting the MediaPlayer object. 260 | a._done = True 261 | a.ffplayer = None 262 | set_log_callback(None) 263 | -------------------------------------------------------------------------------- /ffpyplayer/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | FFPyPlayer library 3 | ================== 4 | ''' 5 | import sys 6 | import site 7 | import os 8 | from os.path import join 9 | import platform 10 | 11 | __all__ = ('dep_bins', ) 12 | 13 | __version__ = '4.5.3.dev0' 14 | version = __version__ 15 | 16 | # the ffmpeg src git version tested and upto date with, 17 | # and including this commit 18 | _ffmpeg_git = 'c926140558c60786dc577b121df6b3c6b430bd98' 19 | # excludes commits bdf9ed41fe4bdf4e254615b7333ab0feb1977e98, 20 | # 1be3d8a0cb77f8d34c1f39b47bf5328fe10c82d7, 21 | # f1907faab4023517af7d10d746b5684cccc5cfcc, and 22 | # 0995e1f1b31f6e937a1b527407ed3e850f138098 because they require ffmpeg 5.1/5.2 23 | # which is too new as of now 24 | 25 | # also skipped all show modes and subtitle display related functionality commits 26 | 27 | # TODO: 28 | # * Implement CONFIG_SDL to be able to compile without needing SDL at all. 29 | # * Currently, it only supports text subtitles - bitmap subtitles are ignored. 30 | # Unless one uses a filter to overlay the subtitle. 31 | # * We can not yet visualize audio to video. Provide a filter chain link between 32 | # audio to video filters to acomplish this. 33 | 34 | dep_bins = [] 35 | '''A list of paths to the binaries used by the library. It can be used during 36 | packaging for including required binaries. 37 | 38 | It is read only. 39 | ''' 40 | 41 | for d in [sys.prefix, site.USER_BASE]: 42 | if d is None: 43 | continue 44 | for lib in ('ffmpeg', 'sdl'): 45 | p = join(d, 'share', 'ffpyplayer', lib, 'bin') 46 | if os.path.isdir(p): 47 | os.environ["PATH"] = p + os.pathsep + os.environ["PATH"] 48 | if hasattr(os, 'add_dll_directory'): 49 | os.add_dll_directory(p) 50 | dep_bins.append(p) 51 | 52 | if 'SDL_AUDIODRIVER' not in os.environ and platform.system() == 'Windows': 53 | os.environ['SDL_AUDIODRIVER'] = 'DirectSound' 54 | -------------------------------------------------------------------------------- /ffpyplayer/clib/misc.c: -------------------------------------------------------------------------------- 1 | 2 | #include "misc.h" 3 | 4 | #define FLAGS (o->type == AV_OPT_TYPE_FLAGS) ? AV_DICT_APPEND : 0 5 | void print_all_libs_info(int flags, int level) 6 | { 7 | #if CONFIG_AVUTIL 8 | PRINT_LIB_INFO(avutil, AVUTIL, flags, level); 9 | #endif 10 | #if CONFIG_AVCODEC 11 | PRINT_LIB_INFO(avcodec, AVCODEC, flags, level); 12 | #endif 13 | #if CONFIG_AVFORMAT 14 | PRINT_LIB_INFO(avformat, AVFORMAT, flags, level); 15 | #endif 16 | #if CONFIG_AVDEVICE 17 | PRINT_LIB_INFO(avdevice, AVDEVICE, flags, level); 18 | #endif 19 | #if CONFIG_AVFILTER 20 | PRINT_LIB_INFO(avfilter, AVFILTER, flags, level); 21 | #endif 22 | #if CONFIG_SWSCALE 23 | PRINT_LIB_INFO(swscale, SWSCALE, flags, level); 24 | #endif 25 | #if CONFIG_SWRESAMPLE 26 | PRINT_LIB_INFO(swresample,SWRESAMPLE, flags, level); 27 | #endif 28 | #if CONFIG_POSTPROC 29 | PRINT_LIB_INFO(postproc, POSTPROC, flags, level); 30 | #endif 31 | } 32 | 33 | const AVOption *opt_find(const void * obj, const char *name, const char *unit, 34 | int opt_flags, int search_flags) 35 | { 36 | const AVOption *o = av_opt_find(obj, name, unit, opt_flags, search_flags); 37 | if(o && !o->flags) 38 | return NULL; 39 | return o; 40 | } 41 | 42 | #define FLAGS (o->type == AV_OPT_TYPE_FLAGS) ? AV_DICT_APPEND : 0 43 | int opt_default(const char *opt, const char *arg, 44 | struct SwsContext *sws_opts, AVDictionary **sws_dict, AVDictionary **swr_opts, 45 | AVDictionary **resample_opts, AVDictionary **format_opts, AVDictionary **codec_opts) 46 | { 47 | const AVOption *o; 48 | int consumed = 0; 49 | char opt_stripped[128]; 50 | const char *p; 51 | const AVClass *cc = avcodec_get_class(); 52 | const AVClass *fc = avformat_get_class(); 53 | #if CONFIG_AVRESAMPLE 54 | const AVClass *rc = avresample_get_class(); 55 | #endif 56 | #if CONFIG_SWRESAMPLE 57 | struct SwrContext *swr; 58 | #endif 59 | const AVClass *sc; 60 | const AVClass *swr_class; 61 | int ret; 62 | #if CONFIG_SWSCALE 63 | struct SwsContext *sws; 64 | #endif 65 | 66 | 67 | if (!strcmp(opt, "debug") || !strcmp(opt, "fdebug")) 68 | av_log_set_level(AV_LOG_DEBUG); 69 | 70 | if (!(p = strchr(opt, ':'))) 71 | p = opt + strlen(opt); 72 | av_strlcpy(opt_stripped, opt, FFMIN(sizeof(opt_stripped), p - opt + 1)); 73 | 74 | if ((o = opt_find(&cc, opt_stripped, NULL, 0, 75 | AV_OPT_SEARCH_CHILDREN | AV_OPT_SEARCH_FAKE_OBJ)) || 76 | ((opt[0] == 'v' || opt[0] == 'a' || opt[0] == 's') && 77 | (o = opt_find(&cc, opt + 1, NULL, 0, AV_OPT_SEARCH_FAKE_OBJ)))) { 78 | av_dict_set(codec_opts, opt, arg, FLAGS); 79 | consumed = 1; 80 | } 81 | if ((o = opt_find(&fc, opt, NULL, 0, 82 | AV_OPT_SEARCH_CHILDREN | AV_OPT_SEARCH_FAKE_OBJ))) { 83 | av_dict_set(format_opts, opt, arg, FLAGS); 84 | if (consumed) 85 | av_log(NULL, AV_LOG_VERBOSE, "Routing option %s to both codec and muxer layer\n", opt); 86 | consumed = 1; 87 | } 88 | #if CONFIG_SWSCALE 89 | sc = sws_get_class(); 90 | if (sws_dict && !consumed && (o = opt_find(&sc, opt, NULL, 0, 91 | AV_OPT_SEARCH_CHILDREN | AV_OPT_SEARCH_FAKE_OBJ))) { 92 | sws = sws_alloc_context(); 93 | ret = av_opt_set(sws, opt, arg, 0); 94 | sws_freeContext(sws); 95 | if (ret < 0) { 96 | av_log(NULL, AV_LOG_ERROR, "Error setting option %s.\n", opt); 97 | return ret; 98 | } 99 | if (sws_opts){ 100 | ret = av_opt_set(sws_opts, opt, arg, 0); 101 | if (ret < 0) { 102 | av_log(NULL, AV_LOG_ERROR, "Error setting option %s for sws_opts.\n", opt); 103 | return ret; 104 | } 105 | } 106 | 107 | av_dict_set(sws_dict, opt, arg, FLAGS); 108 | 109 | consumed = 1; 110 | } 111 | #else 112 | if (!consumed && !strcmp(opt, "sws_flags")) { 113 | av_log(NULL, AV_LOG_WARNING, "Ignoring %s %s, due to disabled swscale\n", opt, arg); 114 | consumed = 1; 115 | } 116 | #endif 117 | #if CONFIG_SWRESAMPLE 118 | swr_class = swr_get_class(); 119 | if (swr_opts && !consumed && (o=opt_find(&swr_class, opt, NULL, 0, 120 | AV_OPT_SEARCH_CHILDREN | AV_OPT_SEARCH_FAKE_OBJ))) { 121 | swr = swr_alloc(); 122 | ret = av_opt_set(swr, opt, arg, 0); 123 | swr_free(&swr); 124 | if (ret < 0) { 125 | av_log(NULL, AV_LOG_ERROR, "Error setting option %s.\n", opt); 126 | return ret; 127 | } 128 | av_dict_set(swr_opts, opt, arg, FLAGS); 129 | consumed = 1; 130 | } 131 | #endif 132 | #if CONFIG_AVRESAMPLE 133 | if (resample_opts && (o=opt_find(&rc, opt, NULL, 0, 134 | AV_OPT_SEARCH_CHILDREN | AV_OPT_SEARCH_FAKE_OBJ))) { 135 | av_dict_set(resample_opts, opt, arg, FLAGS); 136 | consumed = 1; 137 | } 138 | #endif 139 | 140 | if (consumed) 141 | return 0; 142 | return AVERROR_OPTION_NOT_FOUND; 143 | } 144 | 145 | int get_plane_sizes(int size[4], int required_plane[4], enum AVPixelFormat pix_fmt, 146 | int height, const int linesizes[4]) 147 | { 148 | int i, total_size; 149 | memset(required_plane, 0, sizeof(required_plane[0])*4); 150 | 151 | const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(pix_fmt); 152 | memset(size, 0, sizeof(size[0])*4); 153 | 154 | if (!height) 155 | return AVERROR(EINVAL); 156 | 157 | if (!desc || desc->flags & AV_PIX_FMT_FLAG_HWACCEL) 158 | return AVERROR(EINVAL); 159 | 160 | if (linesizes[0] > (INT_MAX - 1024) / height) 161 | return AVERROR(EINVAL); 162 | size[0] = linesizes[0] * height; 163 | 164 | if (desc->flags & AV_PIX_FMT_FLAG_PAL) { 165 | size[1] = 256 * 4; 166 | required_plane[0] = 1; 167 | return size[0] + size[1]; 168 | } 169 | 170 | for (i = 0; i < 4; i++) 171 | required_plane[desc->comp[i].plane] = 1; 172 | 173 | total_size = size[0]; 174 | for (i = 1; i < 4 && required_plane[i]; i++) { 175 | int h, s = (i == 1 || i == 2) ? desc->log2_chroma_h : 0; 176 | h = (height + (1 << s) - 1) >> s; 177 | if (linesizes[i] > INT_MAX / h) 178 | return AVERROR(EINVAL); 179 | size[i] = h * linesizes[i]; 180 | if (total_size > INT_MAX - size[i]) 181 | return AVERROR(EINVAL); 182 | total_size += size[i]; 183 | } 184 | 185 | return total_size; 186 | } 187 | -------------------------------------------------------------------------------- /ffpyplayer/clib/misc.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef _FFINFO_H 3 | #define _FFINFO_H 4 | 5 | #include "../includes/ffconfig.h" 6 | #include "libavcodec/avcodec.h" 7 | #include "libavfilter/avfilter.h" 8 | #include "libavformat/avformat.h" 9 | #include "libavdevice/avdevice.h" 10 | #include "libswscale/swscale.h" 11 | #include "libswresample/swresample.h" 12 | #include "libavutil/opt.h" 13 | #include "libavutil/avstring.h" 14 | #include "libavutil/pixdesc.h" 15 | 16 | 17 | #if CONFIG_POSTPROC 18 | #include "libpostproc/postprocess.h" 19 | #endif 20 | 21 | #ifndef AV_LOG_TRACE 22 | #define AV_LOG_TRACE 56 23 | #endif 24 | 25 | 26 | #define INDENT 1 27 | #define SHOW_VERSION 2 28 | #define SHOW_CONFIG 4 29 | 30 | #define PRINT_LIB_INFO(libname, LIBNAME, flags, level) \ 31 | if (CONFIG_##LIBNAME) { \ 32 | const char *indent = flags & INDENT? " " : ""; \ 33 | if (flags & SHOW_VERSION) { \ 34 | unsigned int version = libname##_version(); \ 35 | av_log(NULL, level, \ 36 | "%slib%-11s %2d.%3d.%3d / %2d.%3d.%3d\n", \ 37 | indent, #libname, \ 38 | LIB##LIBNAME##_VERSION_MAJOR, \ 39 | LIB##LIBNAME##_VERSION_MINOR, \ 40 | LIB##LIBNAME##_VERSION_MICRO, \ 41 | version >> 16, version >> 8 & 0xff, version & 0xff); \ 42 | } \ 43 | if (flags & SHOW_CONFIG) { \ 44 | const char *cfg = libname##_configuration(); \ 45 | av_log(NULL, level, "%s%-11s configuration: %s\n", \ 46 | indent, #libname, cfg); \ 47 | } \ 48 | } 49 | 50 | void print_all_libs_info(int flags, int level); 51 | 52 | const AVOption *opt_find(const void * obj, const char *name, const char *unit, 53 | int opt_flags, int search_flags); 54 | 55 | int opt_default(const char *opt, const char *arg, 56 | struct SwsContext *sws_opts, AVDictionary **sws_dict, AVDictionary **swr_opts, 57 | AVDictionary **resample_opts, AVDictionary **format_opts, AVDictionary **codec_opts); 58 | 59 | int get_plane_sizes(int size[4], int required_plane[4], enum AVPixelFormat pix_fmt, 60 | int height, const int linesizes[4]); 61 | 62 | #endif 63 | -------------------------------------------------------------------------------- /ffpyplayer/includes/ff_consts.pxi: -------------------------------------------------------------------------------- 1 | include "ffconfig.pxi" 2 | 3 | 4 | ''' Minimum SDL audio buffer size, in samples.. ''' 5 | DEF SDL_AUDIO_MIN_BUFFER_SIZE = 512 6 | DEF AUDIO_MIN_BUFFER_SIZE = SDL_AUDIO_MIN_BUFFER_SIZE 7 | ' Calculate actual buffer size keeping in mind not cause too frequent audio callbacks. ' 8 | DEF AUDIO_MAX_CALLBACKS_PER_SEC = 30 9 | 10 | DEF MAX_QUEUE_SIZE = (15 * 1024 * 1024) 11 | DEF MIN_FRAMES = 25 12 | DEF EXTERNAL_CLOCK_MIN_FRAMES = 2 13 | DEF EXTERNAL_CLOCK_MAX_FRAMES = 10 14 | 15 | 'no AV sync correction is done if below the minimum AV sync threshold ' 16 | DEF AV_SYNC_THRESHOLD_MIN = 0.04 17 | 'AV sync correction is done if above the maximum AV sync threshold ' 18 | DEF AV_SYNC_THRESHOLD_MAX = 0.1 19 | 'If a frame duration is longer than this, it will not be duplicated to compensate AV sync' 20 | DEF AV_SYNC_FRAMEDUP_THRESHOLD = 0.1 21 | 'no AV correction is done if too big error' 22 | DEF AV_NOSYNC_THRESHOLD = 10.0 23 | 24 | 'maximum audio speed change to get correct sync' 25 | DEF SAMPLE_CORRECTION_PERCENT_MAX = 10 26 | 27 | 'external clock speed adjustment constants for realtime sources based on buffer fullness' 28 | DEF EXTERNAL_CLOCK_SPEED_MIN = 0.900 29 | DEF EXTERNAL_CLOCK_SPEED_MAX = 1.010 30 | DEF EXTERNAL_CLOCK_SPEED_STEP = 0.001 31 | 32 | 'we use about AUDIO_DIFF_AVG_NB A-V differences to make the average' 33 | DEF AUDIO_DIFF_AVG_NB = 20 34 | 35 | 'polls for possible required screen refresh at least this often, should be less than 1/fps' 36 | DEF REFRESH_RATE = 0.0167 37 | 38 | '''NOTE: the size must be big enough to compensate the hardware audio buffersize size 39 | TODO: We assume that a decoded and resampled frame fits into this buffer''' 40 | DEF SAMPLE_ARRAY_SIZE = (8 * 65536) 41 | 42 | DEF VIDEO_PICTURE_QUEUE_SIZE = 3 43 | DEF SUBPICTURE_QUEUE_SIZE = 16 44 | DEF SAMPLE_QUEUE_SIZE = 9 45 | DEF FRAME_QUEUE_SIZE = max(SAMPLE_QUEUE_SIZE, max(VIDEO_PICTURE_QUEUE_SIZE, SUBPICTURE_QUEUE_SIZE)) 46 | 47 | 48 | DEF FF_LOCK_CREATE = 0 49 | DEF FF_LOCK_OBTAIN = 1 50 | DEF FF_LOCK_RELEASE = 2 51 | DEF FF_LOCK_DESTROY = 3 52 | -------------------------------------------------------------------------------- /ffpyplayer/includes/inline_funcs.pxi: -------------------------------------------------------------------------------- 1 | 2 | cdef extern from "string.h" nogil: 3 | char *strerror(int) 4 | 5 | cdef extern from "errno.h" nogil: 6 | int EINVAL 7 | int EDOM 8 | 9 | cdef extern from "limits.h" nogil: 10 | int INT_MAX 11 | 12 | import sys 13 | 14 | cdef int PY3 = sys.version_info > (3, ) 15 | 16 | cdef inline int FFMAX(int a, int b) nogil: 17 | if a > b: 18 | return a 19 | else: 20 | return b 21 | cdef inline double FFMAXD(double a, double b) nogil: 22 | if a > b: 23 | return a 24 | else: 25 | return b 26 | cdef inline void * FFMAXptr(void *a, void *b) nogil: 27 | if a > b: 28 | return a 29 | else: 30 | return b 31 | cdef inline int FFMIN(int a, int b) nogil: 32 | if a > b: 33 | return b 34 | else: 35 | return a 36 | cdef inline double FFMIND(double a, double b) nogil: 37 | if a > b: 38 | return b 39 | else: 40 | return a 41 | cdef inline void * FFMINptr(void *a, void *b) nogil: 42 | if a > b: 43 | return b 44 | else: 45 | return a 46 | cdef inline int compute_mod(int a, int b) nogil: 47 | if a < 0: 48 | return a%b + b 49 | else: 50 | return a%b 51 | 52 | cdef inline int av_opt_set_int_list(void *obj, const char *name, const void *val, 53 | size_t val_deref_size, uint64_t term, int flags) nogil: 54 | if av_int_list_length_for_size(val_deref_size, val, term) > INT_MAX / val_deref_size: 55 | return AVERROR(EINVAL) 56 | else: 57 | return av_opt_set_bin(obj, name, val,\ 58 | av_int_list_length_for_size(val_deref_size, val, term) * val_deref_size, flags) 59 | 60 | cdef inline int cmp_audio_fmts(AVSampleFormat fmt1, int64_t channel_count1, 61 | AVSampleFormat fmt2, int64_t channel_count2) nogil: 62 | # If channel count == 1, planar and non-planar formats are the same 63 | if channel_count1 == 1 and channel_count2 == 1: 64 | return av_get_packed_sample_fmt(fmt1) != av_get_packed_sample_fmt(fmt2) 65 | else: 66 | return channel_count1 != channel_count2 or fmt1 != fmt2 67 | 68 | cdef inline int64_t get_valid_channel_layout(int64_t channel_layout, int channels) nogil: 69 | if channel_layout and av_get_channel_layout_nb_channels(channel_layout) == channels: 70 | return channel_layout 71 | else: 72 | return 0 73 | 74 | cdef inline char * emsg(int code, char *msg, int buff_size) except NULL: 75 | if av_strerror(code, msg, buff_size) < 0: 76 | if EDOM > 0: 77 | code = -code 78 | return strerror(code) 79 | return msg 80 | 81 | cdef inline char * fmt_err(int code, char *msg, int buff_size) nogil: 82 | if av_strerror(code, msg, buff_size) < 0: 83 | if EDOM > 0: 84 | code = -code 85 | return strerror(code) 86 | return msg 87 | 88 | cdef inline int insert_filt( 89 | const char *name, const char *arg, AVFilterGraph *graph, 90 | AVFilterContext **last_filter) nogil: 91 | cdef int ret 92 | cdef AVFilterContext *filt_ctx 93 | 94 | ret = avfilter_graph_create_filter( 95 | &filt_ctx, avfilter_get_by_name(name), name, arg, NULL, graph) 96 | if ret < 0: 97 | return ret 98 | 99 | ret = avfilter_link(filt_ctx, 0, last_filter[0], 0) 100 | if ret < 0: 101 | return ret 102 | 103 | last_filter[0] = filt_ctx 104 | return 0 105 | 106 | cdef inline object tcode(bytes s): 107 | if PY3: 108 | return s.decode('utf8') 109 | return s 110 | -------------------------------------------------------------------------------- /ffpyplayer/includes/sdl.pxi: -------------------------------------------------------------------------------- 1 | from libc.stdint cimport int64_t, uint64_t, int32_t, uint32_t, uint16_t,\ 2 | int16_t, uint8_t, int8_t, uintptr_t 3 | 4 | cdef extern from "SDL.h" nogil: 5 | int SDL_INIT_VIDEO 6 | int SDL_INIT_AUDIO 7 | int SDL_INIT_TIMER 8 | int SDL_INIT_EVENTTHREAD 9 | 10 | void SDL_Delay(int) 11 | 12 | void SDL_WaitThread(SDL_Thread *, int *) 13 | struct SDL_mutex: 14 | pass 15 | struct SDL_Thread: 16 | pass 17 | struct SDL_cond: 18 | pass 19 | 20 | char *SDL_GetError() 21 | 22 | SDL_cond *SDL_CreateCond() 23 | void SDL_DestroyCond(SDL_cond *) 24 | int SDL_CondSignal(SDL_cond *) 25 | int SDL_CondWait(SDL_cond *, SDL_mutex *) 26 | 27 | void SDL_Quit() 28 | int SDL_Init(uint32_t) with gil 29 | int SDL_InitSubSystem(uint32_t) with gil 30 | 31 | struct SDL_AudioSpec: 32 | int freq 33 | uint16_t format 34 | uint8_t channels 35 | uint8_t silence 36 | uint16_t samples 37 | uint16_t padding 38 | uint32_t size 39 | void (*callback)(void *, uint8_t *, int) 40 | void *userdata 41 | 42 | 43 | cdef extern from "SDL_thread.h" nogil: 44 | SDL_Thread *SDL_CreateThread(int_void_func, const char *, void *) with gil 45 | 46 | IF USE_SDL2_MIXER: 47 | cdef extern from "SDL_mixer.h" nogil: 48 | struct Mix_Chunk: 49 | int allocated 50 | uint8_t *abuf 51 | uint32_t alen 52 | uint8_t volume 53 | 54 | int Mix_OpenAudio(int, uint16_t, int, int) 55 | int Mix_QuerySpec(int *, uint16_t *, int *) 56 | void Mix_CloseAudio() 57 | 58 | Mix_Chunk *Mix_QuickLoad_RAW(uint8_t *, uint32_t) 59 | void Mix_FreeChunk(Mix_Chunk *) 60 | 61 | int Mix_AllocateChannels(int) 62 | int Mix_PlayChannel(int, Mix_Chunk *, int) 63 | int Mix_Volume(int, int) 64 | int Mix_RegisterEffect(int, void (*)(int, void *, int, void *), void (*)(int, void *), void *) 65 | int Mix_UnregisterEffect(int, void (*)(int, void *, int, void *)) 66 | void Mix_Pause(int) 67 | void Mix_Resume(int) 68 | int Mix_HaltChannel(int) 69 | 70 | 71 | cdef extern from * nogil: 72 | uint32_t SDL_HWACCEL 73 | uint32_t SDL_ASYNCBLIT 74 | uint32_t SDL_HWSURFACE 75 | uint32_t SDL_FULLSCREEN 76 | uint32_t SDL_RESIZABLE 77 | uint32_t SDL_YV12_OVERLAY 78 | uint8_t SDL_MIX_MAXVOLUME 79 | 80 | uint16_t AUDIO_S16SYS 81 | int SDL_OpenAudio(SDL_AudioSpec *, SDL_AudioSpec *) 82 | int SDL_AUDIO_ALLOW_ANY_CHANGE 83 | ctypedef uint32_t SDL_AudioDeviceID 84 | SDL_AudioDeviceID SDL_OpenAudioDevice( 85 | const char*, int, const SDL_AudioSpec*, SDL_AudioSpec*, int) 86 | void SDL_PauseAudioDevice(SDL_AudioDeviceID, int) 87 | void SDL_CloseAudioDevice(SDL_AudioDeviceID) 88 | void SDL_MixAudioFormat( 89 | uint8_t*, const uint8_t*, uint16_t, uint32_t, int) 90 | 91 | void SDL_PauseAudio(int) 92 | void SDL_CloseAudio() 93 | void SDL_MixAudio(uint8_t *, const uint8_t *, uint32_t, int) 94 | 95 | SDL_mutex *SDL_CreateMutex() 96 | void SDL_DestroyMutex(SDL_mutex *) 97 | int SDL_mutexP(SDL_mutex *) # SDL_LockMutex 98 | int SDL_mutexV(SDL_mutex *) # SDL_UnlockMutex 99 | int SDL_CondWaitTimeout(SDL_cond *, SDL_mutex *, uint32_t) 100 | 101 | void SDL_UpdateRect(SDL_Surface *, int32_t, int32_t, uint32_t, uint32_t) 102 | int SDL_FillRect(SDL_Surface *, SDL_Rect *, uint32_t) 103 | int SDL_LockYUVOverlay(SDL_Overlay *) 104 | void SDL_UnlockYUVOverlay(SDL_Overlay *) 105 | int SDL_DisplayYUVOverlay(SDL_Overlay *, SDL_Rect *) 106 | void SDL_FreeYUVOverlay(SDL_Overlay *) 107 | uint32_t SDL_MapRGB(const SDL_PixelFormat * const, const uint8_t, 108 | const uint8_t, const uint8_t) 109 | SDL_Overlay * SDL_CreateYUVOverlay(int, int, uint32_t, SDL_Surface *) 110 | 111 | void SDL_WM_SetCaption(const char *, const char *) 112 | int SDL_setenv(const char *, const char *, int) 113 | char * SDL_getenv(const char *) 114 | 115 | SDL_Surface *SDL_SetVideoMode(int, int, int, uint32_t) 116 | const SDL_VideoInfo *SDL_GetVideoInfo() 117 | uint8_t SDL_EventState(uint8_t, int) 118 | void SDL_PumpEvents() 119 | 120 | int SDL_IGNORE 121 | uint8_t SDL_ACTIVEEVENT 122 | uint8_t SDL_SYSWMEVENT 123 | enum: 124 | SDL_VIDEOEXPOSE, 125 | SDL_USEREVENT, 126 | SDL_QUIT, 127 | SDL_VIDEORESIZE, 128 | uint32_t SDL_ALLEVENTS 129 | 130 | struct SDL_VideoInfo: 131 | int current_w 132 | int current_h 133 | struct SDL_Overlay: 134 | int w, h #/**< Read-only */ 135 | uint16_t *pitches #/**< Read-only */ 136 | uint8_t **pixels #/**< Read-write */ 137 | struct SDL_PixelFormat: 138 | pass 139 | struct SDL_Rect: 140 | int16_t x, y 141 | uint16_t w, h 142 | struct SDL_Surface: 143 | SDL_PixelFormat *format 144 | int w, h 145 | 146 | 147 | struct SDL_UserEvent: 148 | uint8_t type 149 | int code 150 | void *data1 151 | void *data2 152 | struct SDL_ResizeEvent: 153 | uint8_t type 154 | int w 155 | int h 156 | union SDL_Event: 157 | uint8_t type 158 | SDL_UserEvent user 159 | SDL_ResizeEvent resize 160 | enum SDL_eventaction: 161 | SDL_ADDEVENT, 162 | SDL_PEEKEVENT, 163 | SDL_GETEVENT, 164 | int SDL_PushEvent(SDL_Event *event) 165 | int SDL_PeepEvents(SDL_Event *, int, SDL_eventaction, uint32_t) 166 | 167 | int SDL_ShowCursor(int) 168 | -------------------------------------------------------------------------------- /ffpyplayer/pic.pxd: -------------------------------------------------------------------------------- 1 | include 'includes/ffmpeg.pxi' 2 | 3 | 4 | cdef class SWScale(object): 5 | cdef SwsContext *sws_ctx 6 | cdef bytes dst_pix_fmt 7 | cdef str dst_pix_fmt_s 8 | cdef int dst_h 9 | cdef int dst_w 10 | cdef AVPixelFormat src_pix_fmt 11 | cdef int src_h 12 | cdef int src_w 13 | 14 | 15 | cdef class Image(object): 16 | 17 | cdef AVFrame *frame 18 | cdef list byte_planes 19 | cdef AVPixelFormat pix_fmt 20 | 21 | cdef int cython_init(self, AVFrame *frame) nogil except 1 22 | cpdef is_ref(Image self) 23 | cpdef is_key_frame(Image self) 24 | cpdef get_linesizes(Image self, keep_align=*) 25 | cpdef get_size(Image self) 26 | cpdef get_pixel_format(Image self) 27 | cpdef get_buffer_size(Image self, keep_align=*) 28 | cpdef get_required_buffers(Image self) 29 | cpdef to_bytearray(Image self, keep_align=*) 30 | cpdef to_memoryview(Image self, keep_align=*) 31 | 32 | 33 | cdef class ImageLoader(object): 34 | cdef AVFormatContext *format_ctx 35 | cdef AVCodec *codec 36 | cdef AVCodecContext *codec_ctx 37 | cdef AVPacket pkt 38 | cdef AVFrame *frame 39 | cdef bytes filename 40 | cdef char msg[256] 41 | cdef int eof 42 | 43 | cpdef next_frame(self) 44 | cdef inline object eof_frame(self) 45 | -------------------------------------------------------------------------------- /ffpyplayer/player/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | FFmpeg based media player 3 | ========================= 4 | 5 | A FFmpeg based python media player. See :class:`MediaPlayer` for details. 6 | ''' 7 | 8 | __all__ = ('MediaPlayer', ) 9 | 10 | from ffpyplayer.player.player import MediaPlayer 11 | -------------------------------------------------------------------------------- /ffpyplayer/player/clock.pxd: -------------------------------------------------------------------------------- 1 | 2 | include '../includes/ffmpeg.pxi' 3 | 4 | 5 | cdef class Clock(object): 6 | cdef: 7 | double pts # clock base 8 | double pts_drift # clock base minus time at which we updated the clock 9 | double last_updated 10 | double speed 11 | int serial # clock is based on a packet with this serial 12 | int paused 13 | int *queue_serial # pointer to the current packet queue serial, used for obsolete clock detection 14 | 15 | cdef void cInit(Clock self, int *queue_serial) nogil 16 | cdef double get_clock(Clock self) nogil 17 | cdef void set_clock_at(Clock self, double pts, int serial, double time) nogil 18 | cdef void set_clock(Clock self, double pts, int serial) nogil 19 | cdef void set_clock_speed(Clock self, double speed) nogil 20 | cdef void sync_clock_to_slave(Clock self, Clock slave) nogil 21 | -------------------------------------------------------------------------------- /ffpyplayer/player/clock.pyx: -------------------------------------------------------------------------------- 1 | #cython: cdivision=True 2 | 3 | __all__ = ('Clock', ) 4 | 5 | include '../includes/ff_consts.pxi' 6 | 7 | cdef extern from "math.h" nogil: 8 | double NAN 9 | int isnan(double x) 10 | double fabs(double x) 11 | 12 | 13 | cdef class Clock(object): 14 | 15 | def __cinit__(Clock self): 16 | pass 17 | cdef void cInit(Clock self, int *queue_serial) nogil: 18 | self.speed = 1.0 19 | self.paused = 0 20 | if queue_serial != NULL: 21 | self.queue_serial = queue_serial 22 | else: 23 | self.queue_serial = &self.serial 24 | self.set_clock(NAN, -1) 25 | 26 | def __dealloc__(Clock self): 27 | pass 28 | 29 | cdef double get_clock(Clock self) nogil: 30 | cdef double time 31 | if self.queue_serial[0] != self.serial: 32 | return NAN 33 | if self.paused: 34 | return self.pts 35 | else: 36 | time = av_gettime_relative() / 1000000.0 37 | return self.pts_drift + time - (time - self.last_updated) * (1.0 - self.speed) 38 | 39 | cdef void set_clock_at(Clock self, double pts, int serial, double time) nogil: 40 | self.pts = pts 41 | self.last_updated = time 42 | self.pts_drift = self.pts - time 43 | self.serial = serial 44 | 45 | cdef void set_clock(Clock self, double pts, int serial) nogil: 46 | cdef double time = av_gettime_relative() / 1000000.0 47 | self.set_clock_at(pts, serial, time) 48 | 49 | cdef void set_clock_speed(Clock self, double speed) nogil: 50 | self.set_clock(self.get_clock(), self.serial) 51 | self.speed = speed 52 | 53 | cdef void sync_clock_to_slave(Clock self, Clock slave) nogil: 54 | cdef double clock = self.get_clock() 55 | cdef double slave_clock = slave.get_clock() 56 | if (not isnan(slave_clock)) and (isnan(clock) or fabs(clock - slave_clock) > AV_NOSYNC_THRESHOLD): 57 | self.set_clock(slave_clock, slave.serial) 58 | -------------------------------------------------------------------------------- /ffpyplayer/player/core.pxd: -------------------------------------------------------------------------------- 1 | 2 | include '../includes/ffmpeg.pxi' 3 | 4 | from ffpyplayer.player.queue cimport FFPacketQueue 5 | from ffpyplayer.player.frame_queue cimport FrameQueue, Frame 6 | from ffpyplayer.player.decoder cimport Decoder 7 | from ffpyplayer.threading cimport MTGenerator, MTThread, MTMutex, MTCond 8 | from ffpyplayer.player.clock cimport Clock 9 | from ffpyplayer.pic cimport Image 10 | from cpython.ref cimport PyObject 11 | 12 | 13 | cdef struct AudioParams: 14 | int freq 15 | int channels 16 | int64_t channel_layout 17 | AVSampleFormat fmt 18 | int frame_size 19 | int bytes_per_sec 20 | 21 | 22 | cdef class VideoState(object): 23 | cdef: 24 | MTThread read_tid 25 | const AVInputFormat *iformat 26 | int abort_request 27 | int paused 28 | int last_paused 29 | int queue_attachments_req 30 | int seek_req 31 | int seek_flags 32 | int64_t seek_pos 33 | int64_t seek_rel 34 | int read_pause_return 35 | AVFormatContext *ic 36 | int realtime 37 | int reached_eof 38 | int eof 39 | int audio_dev 40 | 41 | Clock audclk 42 | Clock vidclk 43 | Clock extclk 44 | 45 | FrameQueue pictq 46 | FrameQueue subpq 47 | FrameQueue sampq 48 | 49 | Decoder auddec 50 | Decoder viddec 51 | Decoder subdec 52 | 53 | int audio_stream 54 | 55 | int av_sync_type 56 | 57 | double audio_clock 58 | int audio_clock_serial 59 | double audio_diff_cum # used for AV difference average computation 60 | double audio_diff_avg_coef 61 | double audio_diff_threshold 62 | int audio_diff_avg_count 63 | AVStream *audio_st 64 | FFPacketQueue audioq 65 | int audio_hw_buf_size 66 | 67 | IF USE_SDL2_MIXER: 68 | uint8_t chunk_buf[AUDIO_MIN_BUFFER_SIZE] 69 | Mix_Chunk *chunk 70 | int audio_count 71 | 72 | uint8_t *audio_buf 73 | uint8_t *audio_buf1 74 | unsigned int audio_buf_size # in bytes 75 | unsigned int audio_buf1_size 76 | int audio_buf_index # in bytes 77 | int audio_write_buf_size 78 | AudioParams audio_src 79 | IF CONFIG_AVFILTER: 80 | AudioParams audio_filter_src 81 | AudioParams audio_tgt 82 | SwrContext *swr_ctx 83 | int frame_drops_early 84 | int frame_drops_late 85 | 86 | int16_t sample_array[SAMPLE_ARRAY_SIZE] 87 | int sample_array_index 88 | 89 | int subtitle_stream 90 | AVStream *subtitle_st 91 | FFPacketQueue subtitleq 92 | 93 | double frame_timer 94 | double frame_last_returned_time 95 | double frame_last_filter_delay 96 | int video_stream 97 | AVStream *video_st 98 | FFPacketQueue videoq 99 | double max_frame_duration # maximum duration of a frame - above this, we consider the jump a timestamp discontinuity 100 | 101 | IF CONFIG_AVFILTER: 102 | int vfilter_idx 103 | AVFilterContext *in_video_filter # the first filter in the video chain 104 | AVFilterContext *out_video_filter # the last filter in the video chain 105 | AVFilterContext *in_audio_filter # the first filter in the audio chain 106 | AVFilterContext *out_audio_filter # the last filter in the audio chain 107 | AVFilterContext *split_audio_filter # the last filter in the audio chain 108 | AVFilterGraph *agraph # audio filter graph 109 | 110 | int last_video_stream, last_audio_stream, last_subtitle_stream 111 | 112 | MTCond continue_read_thread 113 | MTGenerator mt_gen 114 | VideoSettings *player 115 | int64_t last_time 116 | 117 | MTCond pause_cond 118 | double last_clock 119 | PyObject *self_id 120 | 121 | dict metadata 122 | 123 | object callback 124 | int is_ref 125 | AVPixelFormat pix_fmt 126 | 127 | 128 | cdef int cInit(self, MTGenerator mt_gen, VideoSettings *player, int paused, 129 | AVPixelFormat out_fmt) nogil except 1 130 | cdef int cquit(VideoState self) nogil except 1 131 | cdef int request_thread_s(self, char *name, char *msg) nogil except 1 132 | cdef int request_thread(self, char *name, object msg) nogil except 1 133 | cdef int request_thread_py(self, object name, object msg) except 1 134 | cdef object get_out_pix_fmt(self) 135 | cdef void set_out_pix_fmt(self, AVPixelFormat out_fmt) 136 | cdef int get_master_sync_type(VideoState self) nogil 137 | cdef double get_master_clock(VideoState self) nogil except? 0.0 138 | cdef int check_external_clock_speed(VideoState self) nogil except 1 139 | cdef int stream_seek(VideoState self, int64_t pos, int64_t rel, int seek_by_bytes, int flush) nogil except 1 140 | cdef int seek_chapter(VideoState self, int incr, int flush) nogil except 1 141 | cdef int toggle_pause(VideoState self) nogil except 1 142 | cdef double compute_target_delay(VideoState self, double delay) nogil except? 0.0 143 | cdef double vp_duration(VideoState self, Frame *vp, Frame *nextvp) nogil except? 0.0 144 | cdef void update_video_pts(VideoState self, double pts, int64_t pos, int serial) nogil 145 | cdef int video_refresh(VideoState self, Image next_image, double *pts, double *remaining_time, 146 | int force_refresh) nogil except -1 147 | cdef int get_video_frame(VideoState self, AVFrame *frame) nogil except 2 148 | IF CONFIG_AVFILTER: 149 | cdef int configure_filtergraph(VideoState self, AVFilterGraph *graph, const char *filtergraph, 150 | AVFilterContext *source_ctx, AVFilterContext *sink_ctx) nogil except? 1 151 | cdef int configure_video_filters(VideoState self, AVFilterGraph *graph, 152 | const char *vfilters, AVFrame *frame, 153 | AVPixelFormat pix_fmt) nogil except? 1 154 | cdef int configure_audio_filters(VideoState self, const char *afilters, 155 | int force_output_format) nogil except? 1 156 | cdef int audio_thread(self) nogil except? 1 157 | cdef int video_thread(VideoState self) nogil except? 1 158 | cdef int subtitle_thread(VideoState self) nogil except 1 159 | cdef int subtitle_display(self, AVSubtitle *sub) nogil except 1 160 | cdef int update_sample_display(VideoState self, int16_t *samples, int samples_size) nogil except 1 161 | cdef int synchronize_audio(VideoState self, int nb_samples) nogil except -1 162 | cdef int audio_decode_frame(VideoState self) nogil except? 1 163 | cdef int sdl_audio_callback(VideoState self, uint8_t *stream, int len) nogil except 1 164 | cdef inline int open_audio_device(VideoState self, SDL_AudioSpec *wanted_spec, SDL_AudioSpec *spec) nogil except 1 165 | cdef int audio_open(VideoState self, int64_t wanted_channel_layout, int wanted_nb_channels, 166 | int wanted_sample_rate, AudioParams *audio_hw_params) nogil except? 1 167 | cdef int stream_component_open(VideoState self, int stream_index) nogil except 1 168 | cdef int stream_component_close(VideoState self, int stream_index) nogil except 1 169 | cdef int read_thread(VideoState self) nogil except 1 170 | cdef int stream_has_enough_packets(self, AVStream *st, int stream_id, FFPacketQueue queue) nogil 171 | cdef inline int failed(VideoState self, int ret, AVFormatContext *ic, AVPacket **pkt) nogil except 1 172 | cdef int stream_select_program(VideoState self, int requested_program) nogil except 1 173 | cdef int stream_select_channel(VideoState self, int codec_type, unsigned int requested_stream) nogil except 1 174 | cdef int stream_cycle_channel(VideoState self, int codec_type) nogil except 1 175 | cdef int decode_interrupt_cb(VideoState self) nogil 176 | 177 | 178 | cdef struct VideoSettings: 179 | unsigned sws_flags 180 | int loglevel 181 | 182 | const AVInputFormat *file_iformat 183 | char *input_filename 184 | int screen_width 185 | int screen_height 186 | uint8_t audio_volume 187 | int muted 188 | int audio_sdl 189 | int audio_disable 190 | int video_disable 191 | int subtitle_disable 192 | const char* wanted_stream_spec[AVMEDIA_TYPE_NB] 193 | int seek_by_bytes 194 | int show_status 195 | int av_sync_type 196 | int64_t start_time 197 | int64_t duration 198 | int fast 199 | int genpts 200 | int lowres 201 | int decoder_reorder_pts 202 | int autoexit 203 | int loop 204 | int framedrop 205 | int infinite_buffer 206 | char *audio_codec_name 207 | char *subtitle_codec_name 208 | char *video_codec_name 209 | const char **vfilters_list 210 | int nb_vfilters 211 | char *afilters 212 | char *avfilters 213 | 214 | int autorotate 215 | int find_stream_info 216 | int filter_threads 217 | 218 | #/* current context */ 219 | int64_t audio_callback_time 220 | 221 | SwsContext *img_convert_ctx 222 | AVDictionary *format_opts 223 | AVDictionary *codec_opts 224 | AVDictionary *resample_opts 225 | AVDictionary *sws_dict 226 | AVDictionary *swr_opts 227 | -------------------------------------------------------------------------------- /ffpyplayer/player/decoder.pxd: -------------------------------------------------------------------------------- 1 | 2 | include '../includes/ffmpeg.pxi' 3 | 4 | from ffpyplayer.threading cimport MTGenerator, MTCond, MTMutex, MTThread 5 | from ffpyplayer.player.queue cimport FFPacketQueue 6 | from ffpyplayer.player.frame_queue cimport FrameQueue 7 | 8 | 9 | cdef class Decoder(object): 10 | cdef: 11 | AVPacket *pkt 12 | FFPacketQueue queue 13 | AVCodecContext *avctx 14 | int pkt_serial 15 | int finished 16 | int packet_pending 17 | MTCond empty_queue_cond 18 | int64_t start_pts 19 | AVRational start_pts_tb 20 | int64_t next_pts 21 | AVRational next_pts_tb 22 | MTThread decoder_tid 23 | 24 | double seek_req_pos 25 | int seeking 26 | MTGenerator mt_gen 27 | 28 | cdef int decoder_init(self, MTGenerator mt_gen, AVCodecContext *avctx, FFPacketQueue queue, 29 | MTCond empty_queue_cond) nogil except 1 30 | cdef void decoder_destroy(self) nogil 31 | cdef void set_seek_pos(self, double seek_req_pos) nogil 32 | cdef int is_seeking(self) nogil 33 | cdef int decoder_abort(self, FrameQueue fq) nogil except 1 34 | cdef int decoder_start(self, int_void_func func, const char *thread_name, void *arg) nogil except 1 35 | cdef int decoder_decode_frame(self, AVFrame *frame, AVSubtitle *sub, int decoder_reorder_pts) nogil except? 2 36 | -------------------------------------------------------------------------------- /ffpyplayer/player/decoder.pyx: -------------------------------------------------------------------------------- 1 | 2 | __all__ = ('Decoder', ) 3 | 4 | include '../includes/ff_consts.pxi' 5 | 6 | cdef extern from "string.h" nogil: 7 | void * memset(void *, int, size_t) 8 | 9 | cdef extern from "errno.h" nogil: 10 | int ENOSYS 11 | int ENOMEM 12 | int EAGAIN 13 | 14 | 15 | cdef class Decoder(object): 16 | 17 | def __cinit__(Decoder self): 18 | self.avctx = NULL 19 | self.pkt = NULL 20 | 21 | cdef int decoder_init( 22 | self, MTGenerator mt_gen, AVCodecContext *avctx, FFPacketQueue queue, 23 | MTCond empty_queue_cond) nogil except 1: 24 | self.pkt = av_packet_alloc() 25 | 26 | with gil: 27 | self.queue = queue 28 | self.empty_queue_cond = empty_queue_cond 29 | self.mt_gen = mt_gen 30 | if self.pkt == NULL: 31 | raise MemoryError 32 | 33 | self.avctx = avctx 34 | self.packet_pending = self.finished = 0 35 | self.seeking = self.start_pts = self.next_pts = 0 36 | self.seek_req_pos = -1 37 | self.start_pts = AV_NOPTS_VALUE 38 | self.pkt_serial = -1 39 | memset(&self.start_pts_tb, 0, sizeof(self.start_pts_tb)) 40 | memset(&self.next_pts_tb, 0, sizeof(self.next_pts_tb)) 41 | return 0 42 | 43 | cdef void decoder_destroy(self) nogil: 44 | av_packet_free(&self.pkt) 45 | avcodec_free_context(&self.avctx) 46 | 47 | cdef void set_seek_pos(self, double seek_req_pos) nogil: 48 | self.seek_req_pos = seek_req_pos 49 | if seek_req_pos == -1: 50 | self.seeking = 0 51 | 52 | cdef int is_seeking(self) nogil: 53 | return self.seeking and self.seek_req_pos != -1 54 | 55 | cdef int decoder_abort(self, FrameQueue fq) nogil except 1: 56 | self.queue.packet_queue_abort() 57 | fq.frame_queue_signal() 58 | self.decoder_tid.wait_thread(NULL) 59 | with gil: 60 | self.decoder_tid = None 61 | self.queue.packet_queue_flush() 62 | return 0 63 | 64 | cdef int decoder_start(self, int_void_func func, const char *thread_name, void *arg) nogil except 1: 65 | self.queue.packet_queue_start() 66 | with gil: 67 | self.decoder_tid = MTThread(self.mt_gen.mt_src) 68 | self.decoder_tid.create_thread(func, thread_name, arg) 69 | return 0 70 | 71 | cdef int decoder_decode_frame(self, AVFrame *frame, AVSubtitle *sub, int decoder_reorder_pts) nogil except? 2: 72 | cdef int ret = AVERROR(EAGAIN) 73 | cdef int got_frame 74 | cdef AVRational tb 75 | cdef int old_serial 76 | 77 | while True: 78 | if self.queue.serial == self.pkt_serial: 79 | while True: 80 | if self.queue.abort_request: 81 | return -1 82 | 83 | if self.avctx.codec_type == AVMEDIA_TYPE_VIDEO: 84 | ret = avcodec_receive_frame(self.avctx, frame) 85 | if ret >= 0: 86 | if decoder_reorder_pts == -1: 87 | frame.pts = frame.best_effort_timestamp 88 | elif not decoder_reorder_pts: 89 | frame.pts = frame.pkt_dts 90 | 91 | elif self.avctx.codec_type == AVMEDIA_TYPE_AUDIO: 92 | ret = avcodec_receive_frame(self.avctx, frame) 93 | if ret >= 0: 94 | tb.num = 1 95 | tb.den = frame.sample_rate 96 | if frame.pts != AV_NOPTS_VALUE: 97 | frame.pts = av_rescale_q(frame.pts, self.avctx.pkt_timebase, tb) 98 | elif self.next_pts != AV_NOPTS_VALUE: 99 | frame.pts = av_rescale_q(self.next_pts, self.next_pts_tb, tb) 100 | if frame.pts != AV_NOPTS_VALUE: 101 | self.next_pts = frame.pts + frame.nb_samples 102 | self.next_pts_tb = tb 103 | 104 | if ret == AVERROR_EOF: 105 | self.finished = self.pkt_serial 106 | avcodec_flush_buffers(self.avctx) 107 | return 0 108 | if ret >= 0: 109 | return 1 110 | if ret == AVERROR(EAGAIN): 111 | break 112 | 113 | while True: 114 | if not self.queue.nb_packets: 115 | self.empty_queue_cond.lock() 116 | self.empty_queue_cond.cond_signal() 117 | self.empty_queue_cond.unlock() 118 | 119 | if self.packet_pending: 120 | self.packet_pending = 0 121 | else: 122 | old_serial = self.pkt_serial 123 | if self.queue.packet_queue_get(self.pkt, 1, &self.pkt_serial) < 0: 124 | return -1 125 | 126 | if old_serial != self.pkt_serial: 127 | avcodec_flush_buffers(self.avctx) 128 | self.finished = 0 129 | self.seeking = self.seek_req_pos != -1 130 | self.next_pts = self.start_pts 131 | self.next_pts_tb = self.start_pts_tb 132 | 133 | if self.queue.serial == self.pkt_serial: 134 | break 135 | av_packet_unref(self.pkt) 136 | 137 | if self.avctx.codec_type == AVMEDIA_TYPE_SUBTITLE: 138 | got_frame = 0 139 | ret = avcodec_decode_subtitle2(self.avctx, sub, &got_frame, self.pkt) 140 | if ret < 0: 141 | ret = AVERROR(EAGAIN) 142 | else: 143 | if got_frame and self.pkt.data == NULL: 144 | self.packet_pending = 1 145 | if got_frame: 146 | ret = 0 147 | else: 148 | ret = AVERROR(EAGAIN) if self.pkt.data != NULL else AVERROR_EOF 149 | av_packet_unref(self.pkt) 150 | else: 151 | if avcodec_send_packet(self.avctx, self.pkt) == AVERROR(EAGAIN): 152 | av_log(self.avctx, AV_LOG_ERROR, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n") 153 | self.packet_pending = 1 154 | else: 155 | av_packet_unref(self.pkt) 156 | -------------------------------------------------------------------------------- /ffpyplayer/player/frame_queue.pxd: -------------------------------------------------------------------------------- 1 | 2 | include '../includes/ffmpeg.pxi' 3 | 4 | from ffpyplayer.threading cimport MTGenerator, MTCond, MTMutex 5 | from ffpyplayer.player.queue cimport FFPacketQueue 6 | from ffpyplayer.player.core cimport VideoSettings 7 | 8 | cdef struct Frame: 9 | AVFrame *frame 10 | int need_conversion 11 | AVSubtitle sub 12 | int serial 13 | double pts # presentation timestamp for the frame 14 | double duration # estimated duration of the frame 15 | int64_t pos # byte position of the frame in the input file 16 | SDL_Overlay *bmp 17 | int allocated 18 | int reallocate 19 | int width 20 | int height 21 | AVRational sar 22 | AVPixelFormat pix_fmt 23 | 24 | 25 | cdef class FrameQueue(object): 26 | cdef: 27 | MTCond cond 28 | FFPacketQueue pktq 29 | Frame queue[FRAME_QUEUE_SIZE] 30 | int rindex 31 | int windex 32 | int size 33 | int max_size 34 | int keep_last 35 | int rindex_shown 36 | 37 | MTMutex alloc_mutex 38 | int requested_alloc 39 | 40 | cdef void frame_queue_unref_item(self, Frame *vp) nogil 41 | cdef int frame_queue_signal(self) nogil except 1 42 | cdef int is_empty(self) nogil 43 | cdef Frame *frame_queue_peek(self) nogil 44 | cdef Frame *frame_queue_peek_next(self) nogil 45 | cdef Frame *frame_queue_peek_last(self) nogil 46 | cdef Frame *frame_queue_peek_writable(self) nogil 47 | cdef Frame *frame_queue_peek_readable(self) nogil 48 | cdef int frame_queue_push(self) nogil except 1 49 | cdef int frame_queue_next(self) nogil except 1 50 | cdef int frame_queue_prev(self) nogil 51 | cdef int frame_queue_nb_remaining(self) nogil 52 | cdef int64_t frame_queue_last_pos(self) nogil 53 | cdef int copy_picture(self, Frame *vp, AVFrame *src_frame, 54 | VideoSettings *player) nogil except 1 55 | cdef int peep_alloc(self) nogil 56 | cdef int queue_picture( 57 | self, AVFrame *src_frame, double pts, double duration, int64_t pos, 58 | int serial, AVPixelFormat out_fmt, int *abort_request, 59 | VideoSettings *player) nogil except 1 60 | cdef int alloc_picture(self) nogil except 1 61 | cdef int copy_picture(self, Frame *vp, AVFrame *src_frame, 62 | VideoSettings *player) nogil except 1 63 | -------------------------------------------------------------------------------- /ffpyplayer/player/frame_queue.pyx: -------------------------------------------------------------------------------- 1 | 2 | __all__ = ('FrameQueue', ) 3 | 4 | include '../includes/ff_consts.pxi' 5 | include "../includes/inline_funcs.pxi" 6 | 7 | cdef extern from "string.h" nogil: 8 | void * memset(void *, int, size_t) 9 | 10 | cdef void raise_py_exception(msg) nogil except *: 11 | with gil: 12 | raise Exception(tcode(msg)) 13 | 14 | 15 | cdef class FrameQueue(object): 16 | 17 | def __cinit__(FrameQueue self, MTGenerator mt_gen, FFPacketQueue pktq, int max_size, int keep_last): 18 | self.cond = MTCond.__new__(MTCond, mt_gen.mt_src) 19 | self.alloc_mutex = MTMutex.__new__(MTMutex, mt_gen.mt_src) 20 | self.max_size = FFMIN(max_size, FRAME_QUEUE_SIZE) 21 | self.pktq = pktq 22 | cdef int i 23 | 24 | with nogil: 25 | self.requested_alloc = 0 26 | memset(self.queue, 0, sizeof(self.queue)) 27 | self.keep_last = not not keep_last 28 | 29 | for i in range(self.max_size): 30 | self.queue[i].pix_fmt = -1 31 | self.queue[i].frame = av_frame_alloc() 32 | if self.queue[i].frame == NULL: 33 | with gil: 34 | raise_py_exception(b'Could not allocate avframe buffer') 35 | 36 | def __dealloc__(self): 37 | cdef int i 38 | cdef Frame *vp 39 | 40 | with nogil: 41 | for i in range(self.max_size): 42 | vp = &self.queue[i] 43 | self.frame_queue_unref_item(vp) 44 | if vp.need_conversion: 45 | av_freep(&vp.frame.data[0]) 46 | av_frame_free(&vp.frame) 47 | 48 | cdef void frame_queue_unref_item(self, Frame *vp) nogil: 49 | av_frame_unref(vp.frame) 50 | avsubtitle_free(&vp.sub) 51 | 52 | cdef int frame_queue_signal(self) nogil except 1: 53 | self.cond.lock() 54 | self.cond.cond_signal() 55 | self.cond.unlock() 56 | return 0 57 | 58 | cdef int is_empty(self) nogil: 59 | return self.size - self.rindex_shown <= 0 60 | 61 | cdef Frame *frame_queue_peek(self) nogil: 62 | return &self.queue[(self.rindex + self.rindex_shown) % self.max_size] 63 | 64 | cdef Frame *frame_queue_peek_next(self) nogil: 65 | return &self.queue[(self.rindex + self.rindex_shown + 1) % self.max_size] 66 | 67 | cdef Frame *frame_queue_peek_last(self) nogil: 68 | return &self.queue[self.rindex] 69 | 70 | cdef Frame *frame_queue_peek_writable(self) nogil: 71 | # wait until we have space to put a new frame 72 | self.cond.lock() 73 | while self.size >= self.max_size and not self.pktq.abort_request: 74 | self.cond.cond_wait() 75 | self.cond.unlock() 76 | 77 | if self.pktq.abort_request: 78 | return NULL 79 | 80 | return &self.queue[self.windex] 81 | 82 | cdef Frame *frame_queue_peek_readable(self) nogil: 83 | # wait until we have a readable a new frame 84 | self.cond.lock() 85 | while self.size - self.rindex_shown <= 0 and not self.pktq.abort_request: 86 | self.cond.cond_wait() 87 | self.cond.unlock() 88 | 89 | if self.pktq.abort_request: 90 | return NULL 91 | 92 | return &self.queue[(self.rindex + self.rindex_shown) % self.max_size] 93 | 94 | cdef int frame_queue_push(self) nogil except 1: 95 | self.windex += 1 96 | if self.windex == self.max_size: 97 | self.windex = 0 98 | 99 | self.cond.lock() 100 | self.size += 1 101 | self.cond.cond_signal() 102 | self.cond.unlock() 103 | return 0 104 | 105 | cdef int frame_queue_next(self) nogil except 1: 106 | if self.keep_last and not self.rindex_shown: 107 | self.rindex_shown = 1 108 | return 0 109 | 110 | self.frame_queue_unref_item(&self.queue[self.rindex]) 111 | self.rindex += 1 112 | if self.rindex == self.max_size: 113 | self.rindex = 0 114 | 115 | self.cond.lock() 116 | self.size -= 1 117 | self.cond.cond_signal() 118 | self.cond.unlock() 119 | return 0 120 | 121 | cdef int frame_queue_prev(self) nogil: 122 | # TODO: https://github.com/FFmpeg/FFmpeg/commit/37d201aad9f7e7f233955345aee1198421a68f5e 123 | # jump back to the previous frame if available by resetting rindex_shown 124 | cdef int ret = self.rindex_shown 125 | self.rindex_shown = 0 126 | return ret 127 | 128 | cdef int frame_queue_nb_remaining(self) nogil: 129 | # return the number of undisplayed frames in the queue 130 | return self.size - self.rindex_shown 131 | 132 | cdef int64_t frame_queue_last_pos(self) nogil: 133 | cdef Frame *fp = &self.queue[self.rindex] 134 | if self.rindex_shown and fp.serial == self.pktq.serial: 135 | return fp.pos 136 | else: 137 | return -1 138 | 139 | cdef int copy_picture(self, Frame *vp, AVFrame *src_frame, 140 | VideoSettings *player) nogil except 1: 141 | cdef const AVDictionaryEntry *e 142 | cdef const AVClass *cls 143 | cdef const AVOption *o 144 | cdef int ret 145 | 146 | if not vp.need_conversion: 147 | av_frame_unref(vp.frame) 148 | av_frame_move_ref(vp.frame, src_frame) 149 | else: 150 | e = av_dict_get(player.sws_dict, b"sws_flags", NULL, 0) 151 | if e != NULL: 152 | cls = sws_get_class() 153 | o = av_opt_find(&cls, b"sws_flags", NULL, 0, 154 | AV_OPT_SEARCH_FAKE_OBJ); 155 | ret = av_opt_eval_flags(&cls, o, e.value, &player.sws_flags) 156 | if ret < 0: 157 | raise_py_exception(b'Could not av_opt_eval_flags') 158 | 159 | player.img_convert_ctx = sws_getCachedContext(player.img_convert_ctx,\ 160 | vp.width, vp.height, src_frame.format, vp.width, vp.height,\ 161 | vp.pix_fmt, player.sws_flags, NULL, NULL, NULL) 162 | if player.img_convert_ctx == NULL: 163 | av_log(NULL, AV_LOG_FATAL, b"Cannot initialize the conversion context\n") 164 | raise_py_exception(b'Cannot initialize the conversion context.') 165 | sws_scale(player.img_convert_ctx, src_frame.data, src_frame.linesize, 166 | 0, vp.height, vp.frame.data, vp.frame.linesize) 167 | av_frame_unref(src_frame) 168 | return 0 169 | 170 | cdef int alloc_picture(self) nogil except 1: 171 | ''' allocate a picture (needs to do that in main thread to avoid 172 | potential locking problems ''' 173 | cdef Frame *vp 174 | self.alloc_mutex.lock() 175 | if self.requested_alloc: 176 | vp = &self.queue[self.windex] 177 | self.frame_queue_unref_item(vp) 178 | if vp.need_conversion: 179 | av_freep(&vp.frame.data[0]) 180 | 181 | if vp.need_conversion: 182 | if (av_image_alloc(vp.frame.data, vp.frame.linesize, vp.width, 183 | vp.height, vp.pix_fmt, 1) < 0): 184 | av_log(NULL, AV_LOG_FATAL, b"Could not allocate avframe buffer.\n") 185 | raise_py_exception(b'Could not allocate avframe buffer') 186 | 187 | vp.frame.width = vp.width 188 | vp.frame.height = vp.height 189 | vp.frame.format = vp.pix_fmt 190 | 191 | self.cond.lock() 192 | vp.allocated = 1 193 | self.cond.cond_signal() 194 | self.cond.unlock() 195 | self.requested_alloc = 0 196 | self.alloc_mutex.unlock() 197 | return 0 198 | 199 | cdef int peep_alloc(self) nogil: 200 | cdef int requested_alloc = 0 201 | self.alloc_mutex.lock() 202 | requested_alloc = self.requested_alloc 203 | self.alloc_mutex.unlock() 204 | return requested_alloc 205 | 206 | cdef int queue_picture( 207 | self, AVFrame *src_frame, double pts, double duration, int64_t pos, 208 | int serial, AVPixelFormat out_fmt, int *abort_request, 209 | VideoSettings *player) nogil except 1: 210 | cdef Frame *vp 211 | 212 | IF 0:# and defined(DEBUG_SYNC): 213 | av_log(NULL, AV_LOG_DEBUG, b"frame_type=%c pts=%0.3f\n", 214 | av_get_picture_type_char(src_frame.pict_type), pts) 215 | 216 | vp = self.frame_queue_peek_writable() 217 | if vp == NULL: 218 | return -1 219 | 220 | vp.sar = src_frame.sample_aspect_ratio 221 | 222 | # alloc or resize hardware picture buffer 223 | if (vp.reallocate or (not vp.allocated) or 224 | vp.width != src_frame.width or vp.height != src_frame.height 225 | or vp.pix_fmt != out_fmt): 226 | vp.allocated = 0 227 | vp.reallocate = 0 228 | vp.width = src_frame.width 229 | vp.height = src_frame.height 230 | vp.pix_fmt = out_fmt 231 | vp.need_conversion = not CONFIG_AVFILTER and out_fmt != src_frame.format 232 | 233 | # the allocation must be done in the main thread to avoid locking problems. 234 | self.alloc_mutex.lock() 235 | self.requested_alloc = 1 236 | self.alloc_mutex.unlock() 237 | 238 | # wait until the picture is allocated 239 | self.cond.lock() 240 | while (not vp.allocated) and not self.pktq.abort_request: 241 | self.cond.cond_wait() 242 | ''' if the queue is aborted, we have to pop the pending ALLOC event 243 | or wait for the allocation to complete ''' 244 | if self.pktq.abort_request and self.peep_alloc(): 245 | while not vp.allocated and not abort_request[0]: 246 | self.cond.cond_wait() 247 | self.cond.unlock() 248 | 249 | if self.pktq.abort_request: 250 | return -1 251 | 252 | # if the frame is not skipped, then display it 253 | self.copy_picture(vp, src_frame, player) 254 | 255 | vp.pts = pts 256 | vp.duration = duration 257 | vp.pos = pos 258 | vp.serial = serial 259 | self.frame_queue_push() 260 | return 0 261 | -------------------------------------------------------------------------------- /ffpyplayer/player/player.pxd: -------------------------------------------------------------------------------- 1 | 2 | include '../includes/ffmpeg.pxi' 3 | 4 | from ffpyplayer.threading cimport MTGenerator, MTThread, MTMutex 5 | from ffpyplayer.player.core cimport VideoState, VideoSettings 6 | from ffpyplayer.pic cimport Image 7 | 8 | 9 | cdef class MediaPlayer(object): 10 | cdef: 11 | VideoSettings settings 12 | MTGenerator mt_gen 13 | VideoState ivs 14 | Image next_image 15 | int is_closed 16 | dict ff_opts 17 | 18 | cdef void _seek(self, double pts, int relative, int seek_by_bytes, int accurate) nogil 19 | cpdef close_player(self) 20 | -------------------------------------------------------------------------------- /ffpyplayer/player/queue.pxd: -------------------------------------------------------------------------------- 1 | 2 | include '../includes/ffmpeg.pxi' 3 | 4 | from ffpyplayer.threading cimport MTGenerator, MTCond 5 | 6 | cdef struct MyAVPacketList: 7 | AVPacket *pkt 8 | int serial 9 | 10 | 11 | cdef class FFPacketQueue(object): 12 | cdef: 13 | MTGenerator mt_gen 14 | AVFifoBuffer *pkt_list 15 | int nb_packets 16 | int size 17 | int64_t duration 18 | int abort_request 19 | int serial 20 | MTCond cond 21 | 22 | cdef int packet_queue_put_private(FFPacketQueue self, AVPacket *pkt) nogil except 1 23 | cdef int packet_queue_put_nullpacket(FFPacketQueue self, AVPacket *pkt, int stream_index) nogil except 1 24 | cdef int packet_queue_put(FFPacketQueue self, AVPacket *pkt) nogil except 1 25 | cdef int packet_queue_flush(FFPacketQueue self) nogil except 1 26 | cdef int packet_queue_abort(FFPacketQueue self) nogil except 1 27 | cdef int packet_queue_start(FFPacketQueue self) nogil except 1 28 | # return < 0 if aborted, 0 if no packet and > 0 if packet. 29 | cdef int packet_queue_get(FFPacketQueue self, AVPacket *pkt, int block, int *serial) nogil except 0 30 | -------------------------------------------------------------------------------- /ffpyplayer/player/queue.pyx: -------------------------------------------------------------------------------- 1 | 2 | __all__ = ('FFPacketQueue', ) 3 | 4 | include '../includes/ff_consts.pxi' 5 | 6 | from ffpyplayer.threading cimport MTGenerator, MTMutex, MTCond 7 | 8 | 9 | cdef class FFPacketQueue(object): 10 | 11 | def __cinit__(FFPacketQueue self, MTGenerator mt_gen): 12 | self.mt_gen = mt_gen 13 | self.pkt_list = NULL 14 | self.nb_packets = self.size = self.serial = 0 15 | self.duration = 0 16 | 17 | self.pkt_list = av_fifo_alloc(sizeof(MyAVPacketList)) 18 | if self.pkt_list == NULL: 19 | raise MemoryError 20 | 21 | self.cond = MTCond.__new__(MTCond, mt_gen.mt_src) 22 | self.abort_request = 1 23 | 24 | def __dealloc__(self): 25 | if self.cond is None: 26 | return 27 | with nogil: 28 | self.packet_queue_flush() 29 | av_fifo_freep(&self.pkt_list) 30 | 31 | cdef int packet_queue_put_private(FFPacketQueue self, AVPacket *pkt) nogil except 1: 32 | cdef MyAVPacketList pkt1 33 | cdef int ret 34 | 35 | if self.abort_request: 36 | return -1 37 | 38 | if av_fifo_space(self.pkt_list) < sizeof(pkt1): 39 | ret = av_fifo_grow(self.pkt_list, sizeof(pkt1)) 40 | if ret < 0: 41 | return ret 42 | 43 | pkt1.pkt = pkt 44 | pkt1.serial = self.serial 45 | 46 | ret = av_fifo_generic_write(self.pkt_list, &pkt1, sizeof(pkt1), NULL) 47 | if ret < 0: 48 | return ret 49 | self.nb_packets += 1 50 | self.size += pkt1.pkt.size + sizeof(pkt1) 51 | self.duration += pkt1.pkt.duration 52 | #/* XXX: should duplicate packet data in DV case */ 53 | self.cond.cond_signal() 54 | return 0 55 | 56 | cdef int packet_queue_put(FFPacketQueue self, AVPacket *pkt) nogil except 1: 57 | cdef AVPacket *pkt1 = av_packet_alloc() 58 | cdef int ret = -1 59 | 60 | if pkt1 == NULL: 61 | av_packet_unref(pkt) 62 | return -1 63 | av_packet_move_ref(pkt1, pkt) 64 | 65 | self.cond.lock() 66 | ret = self.packet_queue_put_private(pkt1) 67 | self.cond.unlock() 68 | 69 | if ret < 0: 70 | av_packet_free(&pkt1) 71 | 72 | return ret 73 | 74 | cdef int packet_queue_put_nullpacket(FFPacketQueue self, AVPacket *pkt, int stream_index) nogil except 1: 75 | pkt.stream_index = stream_index 76 | return self.packet_queue_put(pkt) 77 | 78 | cdef int packet_queue_flush(FFPacketQueue self) nogil except 1: 79 | cdef MyAVPacketList pkt1 80 | cdef int ret = 0 81 | 82 | self.cond.lock() 83 | while av_fifo_size(self.pkt_list) >= sizeof(pkt1): 84 | ret = av_fifo_generic_read(self.pkt_list, &pkt1, sizeof(pkt1), NULL) 85 | if ret < 0: 86 | break 87 | av_packet_free(&pkt1.pkt) 88 | 89 | self.nb_packets = 0 90 | self.size = 0 91 | self.duration = 0 92 | self.serial += 1 93 | self.cond.unlock() 94 | return ret 95 | 96 | cdef int packet_queue_abort(FFPacketQueue self) nogil except 1: 97 | self.cond.lock() 98 | self.abort_request = 1 99 | self.cond.cond_signal() 100 | self.cond.unlock() 101 | return 0 102 | 103 | cdef int packet_queue_start(FFPacketQueue self) nogil except 1: 104 | self.cond.lock() 105 | self.abort_request = 0 106 | self.serial += 1 107 | self.cond.unlock() 108 | return 0 109 | 110 | # return < 0 if aborted, 0 if no packet and > 0 if packet. 111 | cdef int packet_queue_get(FFPacketQueue self, AVPacket *pkt, int block, int *serial) nogil except 0: 112 | cdef MyAVPacketList pkt1 113 | cdef int ret = 0 114 | 115 | self.cond.lock() 116 | 117 | while True: 118 | if self.abort_request: 119 | ret = -1 120 | break 121 | 122 | if av_fifo_size(self.pkt_list) >= sizeof(pkt1): 123 | ret = av_fifo_generic_read(self.pkt_list, &pkt1, sizeof(pkt1), NULL) 124 | if ret < 0: 125 | break 126 | self.nb_packets -= 1 127 | self.size -= pkt1.pkt.size + sizeof(pkt1) 128 | self.duration -= pkt1.pkt.duration 129 | 130 | av_packet_move_ref(pkt, pkt1.pkt) 131 | if serial != NULL: 132 | serial[0] = pkt1.serial 133 | av_packet_free(&pkt1.pkt) 134 | ret = 1 135 | break 136 | elif not block: 137 | ret = -1 138 | break 139 | else: 140 | self.cond.cond_wait() 141 | self.cond.unlock() 142 | return ret 143 | -------------------------------------------------------------------------------- /ffpyplayer/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matham/ffpyplayer/3684c387e217c965d2fc5eb6fab4dfba196e619b/ffpyplayer/tests/__init__.py -------------------------------------------------------------------------------- /ffpyplayer/tests/common.py: -------------------------------------------------------------------------------- 1 | 2 | __all__ = ('get_media', ) 3 | 4 | from os import environ 5 | from os.path import join, abspath, dirname, exists, pathsep 6 | 7 | from ffpyplayer.tools import set_loglevel, set_log_callback 8 | import logging 9 | 10 | set_log_callback(logger=logging, default_only=True) 11 | set_loglevel('trace') 12 | 13 | 14 | def get_media(fname): 15 | if exists(fname): 16 | return abspath(fname) 17 | 18 | root = dirname(__file__) 19 | if exists(join(root, fname)): 20 | return join(root, fname) 21 | 22 | ex = abspath(join(root, '../../examples', fname)) 23 | if exists(ex): 24 | return ex 25 | 26 | if 'FFPYPLAYER_TEST_DIRS' in environ: 27 | for d in environ['FFPYPLAYER_TEST_DIRS'].split(pathsep): 28 | d = d.strip() 29 | if not d: 30 | continue 31 | 32 | if exists(join(d, fname)): 33 | return join(d, fname) 34 | 35 | raise IOError("{} doesn't exist".format(fname)) 36 | -------------------------------------------------------------------------------- /ffpyplayer/tests/test_pic.py: -------------------------------------------------------------------------------- 1 | 2 | def create_image(size): 3 | from ffpyplayer.pic import Image 4 | 5 | w, h = size 6 | size = w * h * 3 7 | buf = bytearray([int(x * 255 / size) for x in range(size)]) 8 | return Image(plane_buffers=[buf], pix_fmt='rgb24', size=(w, h)) 9 | 10 | 11 | def test_pic(): 12 | from ffpyplayer.pic import SWScale 13 | 14 | size = w, h = 500, 100 15 | img = create_image(size) 16 | 17 | assert not img.is_ref() 18 | assert img.get_size() == (w, h) 19 | 20 | sws = SWScale(w, h, img.get_pixel_format(), ofmt='yuv420p') 21 | 22 | img2 = sws.scale(img) 23 | assert img2.get_pixel_format() == 'yuv420p' 24 | planes = img2.to_bytearray() 25 | assert list(map(len, planes)) == [w * h, w * h / 4, w * h / 4, 0] 26 | -------------------------------------------------------------------------------- /ffpyplayer/tests/test_play.py: -------------------------------------------------------------------------------- 1 | 2 | def test_play(): 3 | from .common import get_media 4 | from ffpyplayer.player import MediaPlayer 5 | import time 6 | 7 | error = [None, ] 8 | 9 | def callback(selector, value): 10 | if selector.endswith('error'): 11 | error[0] = selector, value 12 | 13 | # only video 14 | ff_opts = {'an': True, 'sync': 'video'} 15 | player = MediaPlayer( 16 | get_media('dw11222.mp4'), callback=callback, ff_opts=ff_opts) 17 | 18 | i = 0 19 | while not error[0]: 20 | frame, val = player.get_frame() 21 | if val == 'eof': 22 | break 23 | elif frame is None: 24 | time.sleep(0.001) 25 | else: 26 | img, t = frame 27 | i += 1 28 | 29 | player.close_player() 30 | if error[0]: 31 | raise Exception('{}: {}'.format(*error[0])) 32 | 33 | assert i == 6077 34 | -------------------------------------------------------------------------------- /ffpyplayer/tests/test_write.py: -------------------------------------------------------------------------------- 1 | import time 2 | import math 3 | import pytest 4 | 5 | 6 | def get_image(w, h): 7 | from ffpyplayer.pic import Image 8 | 9 | # Construct images 10 | size = w * h * 3 11 | buf = bytearray([int(x * 255 / size) for x in range(size)]) 12 | img = Image(plane_buffers=[buf], pix_fmt='rgb24', size=(w, h)) 13 | return img 14 | 15 | 16 | def get_gray_image_with_val(w, h, val): 17 | from ffpyplayer.pic import Image 18 | 19 | # Construct images 20 | size = w * h 21 | buf = bytearray([int(val)] * size) 22 | img = Image(plane_buffers=[buf], pix_fmt='gray', size=(w, h)) 23 | return img 24 | 25 | 26 | def verify_frames(filename, timestamps, frame_vals=None): 27 | from ffpyplayer.player import MediaPlayer 28 | error = [None, ] 29 | 30 | def callback(selector, value): 31 | if selector.endswith('error'): 32 | error[0] = selector, value 33 | 34 | player = MediaPlayer(filename, callback=callback) 35 | 36 | read_timestamps = set() 37 | try: 38 | i = -1 39 | while not error[0]: 40 | frame, val = player.get_frame() 41 | if val == 'eof': 42 | break 43 | if val == 'paused': 44 | raise ValueError('Got paused') 45 | elif frame is None: 46 | time.sleep(0.01) 47 | else: 48 | img, t = frame 49 | print(i, t) 50 | if i < 0: 51 | i += 1 52 | continue 53 | 54 | print(i, t, timestamps[i]) 55 | read_timestamps.add(t) 56 | assert math.isclose(t, timestamps[i], rel_tol=.1) 57 | 58 | if frame_vals: 59 | assert frame_vals[i] == img.to_bytearray()[0][0] 60 | 61 | i += 1 62 | finally: 63 | player.close_player() 64 | 65 | if error[0] is not None: 66 | raise Exception('{}: {}'.format(*error[0])) 67 | 68 | assert len(timestamps) - 1 == i 69 | assert len(read_timestamps) == i 70 | 71 | 72 | def test_write_streams(tmp_path): 73 | from ffpyplayer.writer import MediaWriter 74 | from ffpyplayer.tools import get_supported_pixfmts, get_supported_framerates 75 | from ffpyplayer.pic import Image 76 | from ffpyplayer.tools import get_codecs 77 | fname = str(tmp_path / 'test_video.avi') 78 | 79 | lib_opts = {} 80 | codec = 'rawvideo' 81 | if 'libx264' in get_codecs(encode=True, video=True): 82 | codec = 'libx264' 83 | lib_opts = {'preset': 'slow', 'crf': '22'} 84 | 85 | w, h = 640, 480 86 | out_opts = { 87 | 'pix_fmt_in': 'rgb24', 'width_in': w, 'height_in': h, 88 | 'codec': codec, 'frame_rate': (5, 1)} 89 | 90 | metadata = { 91 | 'title': 'Singing in the sun', 'author': 'Rat', 92 | 'genre': 'Animal sounds'} 93 | writer = MediaWriter(fname, [out_opts] * 2, fmt='mp4', 94 | width_out=w/2, height_out=h/2, pix_fmt_out='yuv420p', 95 | lib_opts=lib_opts, metadata=metadata) 96 | 97 | # Construct images 98 | size = w * h * 3 99 | buf = bytearray([int(x * 255 / size) for x in range(size)]) 100 | img = Image(plane_buffers=[buf], pix_fmt='rgb24', size=(w, h)) 101 | 102 | buf = bytearray([int((size - x) * 255 / size) for x in range(size)]) 103 | img2 = Image(plane_buffers=[buf], pix_fmt='rgb24', size=(w, h)) 104 | 105 | for i in range(20): 106 | writer.write_frame(img=img, pts=i / 5., stream=0) # stream 1 107 | writer.write_frame(img=img2, pts=i / 5., stream=1) # stream 2 108 | writer.close() 109 | 110 | 111 | @pytest.mark.parametrize('fmt', [('mkv', 'matroska'), ('avi', 'avi')]) 112 | def test_write_correct_frame_rate(tmp_path, fmt): 113 | from ffpyplayer.writer import MediaWriter 114 | fname = str(tmp_path / 'test_frame.') + fmt[0] 115 | 116 | w, h = 64, 64 117 | out_opts = { 118 | 'pix_fmt_in': 'gray', 'width_in': w, 'height_in': h, 119 | 'codec': 'rawvideo', 'frame_rate': (2997, 100)} 120 | 121 | writer = MediaWriter(fname, [out_opts], fmt=fmt[1]) 122 | 123 | timestamps = [] 124 | image_vals = [] 125 | for i in range(20): 126 | timestamps.append(i / 29.97) 127 | image_vals.append(i * 5) 128 | 129 | writer.write_frame( 130 | img=get_gray_image_with_val(w, h, i * 5), pts=i / 29.97, stream=0) 131 | writer.close() 132 | 133 | verify_frames(fname, timestamps, image_vals) 134 | 135 | 136 | @pytest.mark.parametrize('fmt', [('mkv', 'matroska'), ('avi', 'avi')]) 137 | def test_write_larger_than_frame_rate(tmp_path, fmt): 138 | from ffpyplayer.writer import MediaWriter 139 | fname = str(tmp_path / 'test_frame.') + fmt[0] 140 | 141 | w, h = 64, 64 142 | out_opts = { 143 | 'pix_fmt_in': 'gray', 'width_in': w, 'height_in': h, 144 | 'codec': 'rawvideo', 'frame_rate': (15, 1)} 145 | 146 | writer = MediaWriter(fname, [out_opts], fmt=fmt[1]) 147 | 148 | timestamps = [] 149 | image_vals = [] 150 | for i in range(20): 151 | timestamps.append(i) 152 | image_vals.append(i * 5) 153 | 154 | writer.write_frame( 155 | img=get_gray_image_with_val(w, h, i * 5), pts=i, stream=0) 156 | writer.close() 157 | 158 | verify_frames(fname, timestamps, image_vals) 159 | 160 | 161 | @pytest.mark.parametrize('fmt', [('mkv', 'matroska'), ('avi', 'avi')]) 162 | def test_write_smaller_than_frame_rate(tmp_path, fmt): 163 | from ffpyplayer.writer import MediaWriter 164 | fname = str(tmp_path / 'test_frame.') + fmt[0] 165 | 166 | w, h = 64, 64 167 | out_opts = { 168 | 'pix_fmt_in': 'rgb24', 'width_in': w, 'height_in': h, 169 | 'codec': 'rawvideo', 'pix_fmt_out': 'yuv420p', 170 | 'frame_rate': (30, 1)} 171 | 172 | writer = MediaWriter(fname, [out_opts], fmt=fmt[1]) 173 | img = get_image(w, h) 174 | 175 | if fmt[0] == 'avi': 176 | with pytest.raises(Exception): 177 | for i in range(20): 178 | writer.write_frame(img=img, pts=i / 300, stream=0) 179 | else: 180 | for i in range(20): 181 | writer.write_frame(img=img, pts=i / 300, stream=0) 182 | writer.close() 183 | -------------------------------------------------------------------------------- /ffpyplayer/threading.pxd: -------------------------------------------------------------------------------- 1 | 2 | include "includes/ffmpeg.pxi" 3 | 4 | 5 | cdef enum MT_lib: 6 | SDL_MT, 7 | Py_MT 8 | 9 | cdef class MTMutex(object): 10 | cdef MT_lib lib 11 | cdef void* mutex 12 | 13 | cdef int lock(MTMutex self) nogil except 2 14 | cdef int _lock_py(MTMutex self) nogil except 2 15 | cdef int unlock(MTMutex self) nogil except 2 16 | cdef int _unlock_py(MTMutex self) nogil except 2 17 | 18 | cdef class MTCond(object): 19 | cdef MT_lib lib 20 | cdef MTMutex mutex 21 | cdef void *cond 22 | 23 | cdef int lock(MTCond self) nogil except 2 24 | cdef int unlock(MTCond self) nogil except 2 25 | cdef int cond_signal(MTCond self) nogil except 2 26 | cdef int _cond_signal_py(MTCond self) nogil except 2 27 | cdef int cond_wait(MTCond self) nogil except 2 28 | cdef int _cond_wait_py(MTCond self) nogil except 2 29 | cdef int cond_wait_timeout(MTCond self, uint32_t val) nogil except 2 30 | cdef int _cond_wait_timeout_py(MTCond self, uint32_t val) nogil except 2 31 | 32 | cdef class MTThread(object): 33 | cdef MT_lib lib 34 | cdef void* thread 35 | 36 | cdef int create_thread(MTThread self, int_void_func func, const char *thread_name, void *arg) nogil except 2 37 | cdef int wait_thread(MTThread self, int *status) nogil except 2 38 | 39 | 40 | cdef class MTGenerator(object): 41 | cdef MT_lib mt_src 42 | 43 | cdef int delay(MTGenerator self, int delay) nogil except 2 44 | cdef lockmgr_func get_lockmgr(MTGenerator self) nogil 45 | 46 | cdef lockmgr_func get_lib_lockmgr(MT_lib lib) nogil 47 | -------------------------------------------------------------------------------- /ffpyplayer/threading.pyx: -------------------------------------------------------------------------------- 1 | 2 | __all__ = ('MTGenerator', ) 3 | 4 | include "includes/ff_consts.pxi" 5 | include "includes/inline_funcs.pxi" 6 | 7 | from cpython.ref cimport PyObject 8 | 9 | cdef extern from "Python.h": 10 | void Py_INCREF(PyObject *) 11 | void Py_XINCREF(PyObject *) 12 | void Py_DECREF(PyObject *) 13 | 14 | ctypedef int (*int_cls_method)(void *) nogil 15 | 16 | import traceback 17 | 18 | cdef int sdl_initialized = 0 19 | def initialize_sdl(): 20 | '''Initializes sdl. Must be called before anything can be used. 21 | It is automatically called by the modules that use SDL. 22 | ''' 23 | global sdl_initialized 24 | if sdl_initialized: 25 | return 26 | if SDL_Init(0): 27 | raise ValueError('Could not initialize SDL - %s' % SDL_GetError()) 28 | sdl_initialized = 1 29 | initialize_sdl() 30 | 31 | 32 | cdef class MTMutex(object): 33 | 34 | def __cinit__(MTMutex self, MT_lib lib): 35 | self.lib = lib 36 | self.mutex = NULL 37 | if lib == SDL_MT: 38 | self.mutex = SDL_CreateMutex() 39 | if self.mutex == NULL: 40 | raise Exception('Cannot create mutex.') 41 | elif lib == Py_MT: 42 | import threading 43 | mutex = threading.Lock() 44 | self.mutex = mutex 45 | Py_INCREF(self.mutex) 46 | 47 | def __dealloc__(MTMutex self): 48 | if self.lib == SDL_MT: 49 | if self.mutex != NULL: 50 | SDL_DestroyMutex(self.mutex) 51 | elif self.lib == Py_MT: 52 | Py_DECREF(self.mutex) 53 | 54 | cdef int lock(MTMutex self) nogil except 2: 55 | if self.lib == SDL_MT: 56 | return SDL_mutexP(self.mutex) 57 | elif self.lib == Py_MT: 58 | return self._lock_py() 59 | 60 | cdef int _lock_py(MTMutex self) nogil except 2: 61 | with gil: 62 | return not (self.mutex).acquire() 63 | 64 | cdef int unlock(MTMutex self) nogil except 2: 65 | if self.lib == SDL_MT: 66 | return SDL_mutexV(self.mutex) 67 | elif self.lib == Py_MT: 68 | return self._unlock_py() 69 | 70 | cdef int _unlock_py(MTMutex self) nogil except 2: 71 | with gil: 72 | (self.mutex).release() 73 | return 0 74 | 75 | cdef class MTCond(object): 76 | 77 | def __cinit__(MTCond self, MT_lib lib): 78 | self.lib = lib 79 | self.mutex = MTMutex.__new__(MTMutex, lib) 80 | self.cond = NULL 81 | if self.lib == SDL_MT: 82 | self.cond = SDL_CreateCond() 83 | if self.cond == NULL: 84 | raise Exception('Cannot create condition.') 85 | elif self.lib == Py_MT: 86 | import threading 87 | cond = threading.Condition(self.mutex.mutex) 88 | self.cond = cond 89 | Py_INCREF(self.cond) 90 | 91 | def __dealloc__(MTCond self): 92 | if self.lib == SDL_MT: 93 | if self.cond != NULL: 94 | SDL_DestroyCond(self.cond) 95 | elif self.lib == Py_MT: 96 | Py_DECREF(self.cond) 97 | 98 | cdef int lock(MTCond self) nogil except 2: 99 | self.mutex.lock() 100 | 101 | cdef int unlock(MTCond self) nogil except 2: 102 | self.mutex.unlock() 103 | 104 | cdef int cond_signal(MTCond self) nogil except 2: 105 | if self.lib == SDL_MT: 106 | return SDL_CondSignal(self.cond) 107 | elif self.lib == Py_MT: 108 | return self._cond_signal_py() 109 | 110 | cdef int _cond_signal_py(MTCond self) nogil except 2: 111 | with gil: 112 | (self.cond).notify() 113 | return 0 114 | 115 | cdef int cond_wait(MTCond self) nogil except 2: 116 | if self.lib == SDL_MT: 117 | return SDL_CondWait(self.cond, self.mutex.mutex) 118 | elif self.lib == Py_MT: 119 | return self._cond_wait_py() 120 | 121 | cdef int _cond_wait_py(MTCond self) nogil except 2: 122 | with gil: 123 | (self.cond).wait() 124 | return 0 125 | 126 | cdef int cond_wait_timeout(MTCond self, uint32_t val) nogil except 2: 127 | if self.lib == SDL_MT: 128 | return SDL_CondWaitTimeout(self.cond, self.mutex.mutex, val) 129 | elif self.lib == Py_MT: 130 | return self._cond_wait_timeout_py(val) 131 | 132 | cdef int _cond_wait_timeout_py(MTCond self, uint32_t val) nogil except 2: 133 | with gil: 134 | (self.cond).wait(val / 1000.) 135 | return 0 136 | 137 | def enterance_func(target_func, target_arg): 138 | return (target_func)(target_arg) 139 | 140 | cdef class MTThread(object): 141 | 142 | def __cinit__(MTThread self, MT_lib lib): 143 | self.lib = lib 144 | self.thread = NULL 145 | 146 | def __dealloc__(MTThread self): 147 | if self.lib == Py_MT and self.thread != NULL: 148 | Py_DECREF(self.thread) 149 | 150 | cdef int create_thread(MTThread self, int_void_func func, const char *thread_name, void *arg) nogil except 2: 151 | if self.lib == SDL_MT: 152 | with gil: 153 | self.thread = SDL_CreateThread(func, thread_name, arg) 154 | if self.thread == NULL: 155 | raise Exception('Cannot create thread.') 156 | elif self.lib == Py_MT: 157 | with gil: 158 | import threading 159 | thread = threading.Thread(group=None, target=enterance_func, 160 | name=None, args=(func, arg), kwargs={}) 161 | self.thread = thread 162 | Py_INCREF(self.thread) 163 | thread.start() 164 | return 0 165 | 166 | cdef int wait_thread(MTThread self, int *status) nogil except 2: 167 | if self.lib == SDL_MT: 168 | if self.thread != NULL: 169 | SDL_WaitThread(self.thread, status) 170 | elif self.lib == Py_MT: 171 | with gil: 172 | (self.thread).join() 173 | if status != NULL: 174 | status[0] = 0 175 | return 0 176 | 177 | 178 | cdef int_cls_method mutex_lock = MTMutex.lock 179 | cdef int_cls_method mutex_release = MTMutex.unlock 180 | 181 | cdef int _SDL_lockmgr_py(void ** mtx, int op) with gil: 182 | cdef bytes msg 183 | cdef int res = 1 184 | cdef MTMutex mutex 185 | 186 | try: 187 | if op == FF_LOCK_CREATE: 188 | mutex = MTMutex.__new__(MTMutex, SDL_MT) 189 | Py_INCREF(mutex) 190 | mtx[0] = mutex 191 | res = 0 192 | elif op == FF_LOCK_DESTROY: 193 | if mtx[0] != NULL: 194 | Py_DECREF(mtx[0]) 195 | res = 0 196 | except: 197 | msg = traceback.format_exc().encode('utf8') 198 | av_log(NULL, AV_LOG_ERROR, '%s', msg) 199 | return res 200 | 201 | cdef int SDL_lockmgr(void ** mtx, int op) nogil: 202 | if op == FF_LOCK_OBTAIN: 203 | return not not mutex_lock(mtx[0]) 204 | elif op == FF_LOCK_RELEASE: 205 | return not not mutex_release(mtx[0]) 206 | else: 207 | return _SDL_lockmgr_py(mtx, op) 208 | 209 | cdef int Py_lockmgr(void ** mtx, int op) with gil: 210 | cdef int res = 1 211 | cdef bytes msg 212 | cdef MTMutex mutex 213 | 214 | try: 215 | if op == FF_LOCK_CREATE: 216 | mutex = MTMutex.__new__(MTMutex, Py_MT) 217 | Py_INCREF(mutex) 218 | mtx[0] = mutex 219 | res = 0 220 | elif op == FF_LOCK_OBTAIN: 221 | mutex = mtx[0] 222 | res = not not mutex.lock() # force it to 0, or 1 223 | elif op == FF_LOCK_RELEASE: 224 | mutex = mtx[0] 225 | res = not not mutex.unlock() 226 | elif op == FF_LOCK_DESTROY: 227 | if mtx[0] != NULL: 228 | Py_DECREF(mtx[0]) 229 | res = 0 230 | except: 231 | msg = traceback.format_exc().encode('utf8') 232 | av_log(NULL, AV_LOG_ERROR, '%s', msg) 233 | res = 1 234 | return res 235 | 236 | 237 | cdef lockmgr_func get_lib_lockmgr(MT_lib lib) nogil: 238 | if lib == SDL_MT: 239 | return SDL_lockmgr 240 | elif lib == Py_MT: 241 | return Py_lockmgr 242 | 243 | 244 | cdef class MTGenerator(object): 245 | 246 | def __cinit__(MTGenerator self, MT_lib mt_src, **kwargs): 247 | self.mt_src = mt_src 248 | 249 | cdef int delay(MTGenerator self, int delay) nogil except 2: 250 | if self.mt_src == SDL_MT: 251 | SDL_Delay(delay) 252 | elif self.mt_src == Py_MT: 253 | with gil: 254 | import time 255 | time.sleep(delay / 1000.) 256 | return 0 257 | 258 | cdef lockmgr_func get_lockmgr(MTGenerator self) nogil: 259 | return get_lib_lockmgr(self.mt_src) 260 | -------------------------------------------------------------------------------- /ffpyplayer/writer.pxd: -------------------------------------------------------------------------------- 1 | include 'includes/ffmpeg.pxi' 2 | 3 | 4 | cdef class MediaWriter(object): 5 | cdef AVFormatContext *fmt_ctx 6 | cdef MediaStream *streams 7 | cdef int n_streams 8 | cdef list config 9 | cdef AVDictionary *format_opts 10 | cdef int64_t total_size 11 | cdef int closed 12 | 13 | cpdef close(self) 14 | cdef void clean_up(MediaWriter self) nogil 15 | 16 | 17 | cdef struct MediaStream: 18 | # pointer to the stream to which we're adding frames. 19 | AVStream *av_stream 20 | int index 21 | AVCodec *codec 22 | AVCodecContext *codec_ctx 23 | # codec used to encode video 24 | AVCodecID codec_id 25 | # the size of the frame passed in 26 | int width_in 27 | int width_out 28 | # the size of the frame actually written to disk 29 | int height_in 30 | int height_out 31 | # The denominator of the frame rate of the stream 32 | int den 33 | # The numerator of the frame rate of the stream 34 | int num 35 | # the pixel format of the frame passed in 36 | AVPixelFormat pix_fmt_in 37 | # the pixel format of the frame actually written to disk 38 | # if it's -1 (AV_PIX_FMT_NONE) then input will be used. ''' 39 | AVPixelFormat pix_fmt_out 40 | 41 | # The frame in which the final image to be written to disk is held, when we 42 | # need to convert. 43 | AVFrame *av_frame 44 | SwsContext *sws_ctx 45 | int count 46 | int64_t pts 47 | int sync_fmt 48 | 49 | AVDictionary *codec_opts 50 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools", "wheel", "cython~=3.0.11", 4 | ] 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, Extension 2 | from os.path import join, exists, isdir, dirname, abspath 3 | from os import environ, listdir, mkdir 4 | from distutils.command.build_ext import build_ext 5 | import sys 6 | import ffpyplayer 7 | 8 | 9 | # Determine on which platform we are 10 | platform = sys.platform 11 | 12 | # detect Python for android project (http://github.com/kivy/python-for-android) 13 | # or kivy-ios (http://github.com/kivy/kivy-ios) 14 | ndkplatform = environ.get('NDKPLATFORM') 15 | if ndkplatform is not None and environ.get('LIBLINK'): 16 | platform = 'android' 17 | kivy_ios_root = environ.get('KIVYIOSROOT', None) 18 | if kivy_ios_root is not None: 19 | platform = 'ios' 20 | 21 | 22 | # There are issues with using cython at all on some platforms; 23 | # exclude them from using or declaring cython. 24 | 25 | # This determines whether Cython specific functionality may be used. 26 | can_use_cython = True 27 | # This sets whether or not Cython gets added to setup_requires. 28 | declare_cython = False 29 | 30 | if platform in ('ios', 'android'): 31 | # NEVER use or declare cython on these platforms 32 | print('Not using cython on %s' % platform) 33 | can_use_cython = False 34 | else: 35 | declare_cython = True 36 | 37 | src_path = build_path = dirname(__file__) 38 | print(f'Source/build path: {src_path}') 39 | 40 | # select which ffmpeg libraries will be available 41 | c_options = { 42 | # If true, filters will be used' 43 | 'config_avfilter': True, 44 | 'config_avdevice': True, 45 | 'config_swscale': True, 46 | 'config_rtsp_demuxer': True, 47 | 'config_mmsh_protocol': True, 48 | 'config_postproc': platform != 'win32', 49 | # whether sdl is included as an option 50 | 'config_sdl': True, # not implemented yet 51 | 'has_sdl2': True, 52 | 'use_sdl2_mixer': False, 53 | # these should be true 54 | 'config_avutil':True, 55 | 'config_avcodec':True, 56 | 'config_avformat':True, 57 | 'config_swresample':True 58 | } 59 | 60 | for key in list(c_options.keys()): 61 | if key == 'has_sdl2': 62 | continue 63 | 64 | ukey = key.upper() 65 | if ukey in environ: 66 | value = bool(int(environ[ukey])) 67 | print('Environ change {0} -> {1}'.format(key, value)) 68 | c_options[key] = value 69 | 70 | if (not c_options['config_avfilter']) and not c_options['config_swscale']: 71 | raise Exception( 72 | 'At least one of config_avfilter and config_swscale must be enabled.') 73 | 74 | # if c_options['config_avfilter'] and ((not c_options['config_postproc']) or \ 75 | # not c_options['config_swscale']): 76 | # raise Exception( 77 | # 'config_avfilter requires the postproc and swscale binaries.') 78 | c_options['config_avutil'] = c_options['config_avutil'] = True 79 | c_options['config_avformat'] = c_options['config_swresample'] = True 80 | 81 | 82 | class FFBuildExt(build_ext, object): 83 | 84 | def __new__(cls, *a, **kw): 85 | # Note how this class is declared as a subclass of distutils 86 | # build_ext as the Cython version may not be available in the 87 | # environment it is initially started in. However, if Cython 88 | # can be used, setuptools will bring Cython into the environment 89 | # thus its version of build_ext will become available. 90 | # The reason why this is done as a __new__ rather than through a 91 | # factory function is because there are distutils functions that check 92 | # the values provided by cmdclass with issublcass, and so it would 93 | # result in an exception. 94 | # The following essentially supply a dynamically generated subclass 95 | # that mix in the cython version of build_ext so that the 96 | # functionality provided will also be executed. 97 | if can_use_cython: 98 | from Cython.Distutils import build_ext as cython_build_ext 99 | build_ext_cls = type( 100 | 'FFBuildExt', (FFBuildExt, cython_build_ext), {}) 101 | return super(FFBuildExt, cls).__new__(build_ext_cls) 102 | else: 103 | return super(FFBuildExt, cls).__new__(cls) 104 | 105 | def finalize_options(self): 106 | retval = super(FFBuildExt, self).finalize_options() 107 | global build_path 108 | if (self.build_lib is not None and exists(self.build_lib) and 109 | not self.inplace): 110 | build_path = self.build_lib 111 | print(f'Build path changed to: {src_path}') 112 | return retval 113 | 114 | def build_extensions(self): 115 | compiler = self.compiler.compiler_type 116 | print('Using compiler "{}"'.format(compiler)) 117 | 118 | args = [] 119 | link_args = [] 120 | if compiler != 'msvc': 121 | args += ["-O3", '-fno-strict-aliasing', '-Wno-error'] 122 | 123 | if platform == 'darwin': 124 | link_args.append('-headerpad_max_install_names') 125 | print('Using compiler args: {}'.format(args)) 126 | print('Using linker args: {}'.format(link_args)) 127 | 128 | for ext in self.extensions: 129 | ext.extra_compile_args = args 130 | ext.extra_link_args = link_args 131 | super(FFBuildExt, self).build_extensions() 132 | 133 | 134 | cmdclass = {'build_ext': FFBuildExt} 135 | 136 | 137 | def getoutput(cmd): 138 | import subprocess 139 | p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, 140 | stderr=subprocess.PIPE) 141 | p.wait() 142 | if p.returncode: # if not returncode == 0 143 | print('WARNING: A problem occured while running {0} (code {1})\n' 144 | .format(cmd, p.returncode)) 145 | stderr_content = p.stderr.read() 146 | if stderr_content: 147 | print('{0}\n'.format(stderr_content)) 148 | return "" 149 | return p.stdout.read() 150 | 151 | 152 | def pkgconfig(*packages, **kw): 153 | flag_map = {'-I': 'include_dirs', '-L': 'library_dirs', '-l': 'libraries'} 154 | cmd = 'pkg-config --libs --cflags {}'.format(' '.join(packages)) 155 | results = getoutput(cmd).split() 156 | for token in results: 157 | ext = token[:2].decode('utf-8') 158 | flag = flag_map.get(ext) 159 | if not flag: 160 | continue 161 | kw.setdefault(flag, []).append(token[2:].decode('utf-8')) 162 | return kw 163 | 164 | 165 | def get_paths(name): 166 | root = environ.get('{}_ROOT'.format(name)) 167 | print('{}_ROOT: "{}"'.format(name, root)) 168 | if root is not None and not isdir(root): 169 | print('Root directory "{}" is not valid'.format(root)) 170 | root = None 171 | 172 | if root is not None: 173 | include = environ.get( 174 | '{}_INCLUDE_DIR'.format(name), join(root, 'include')) 175 | lib = environ.get('{}_LIB_DIR'.format(name), join(root, 'lib')) 176 | else: 177 | include = environ.get('{}_INCLUDE_DIR'.format(name)) 178 | lib = environ.get('{}_LIB_DIR'.format(name)) 179 | 180 | if include is not None and not isdir(include): 181 | print('Include directory "{}" is not valid'.format(include)) 182 | include = None 183 | if lib is not None and not isdir(lib): 184 | print('Lib directory "{}" is not valid'.format(lib)) 185 | lib = None 186 | return lib, include 187 | 188 | 189 | libraries = [] 190 | library_dirs = [] 191 | include_dirs = [] 192 | 193 | if "KIVYIOSROOT" in environ: 194 | # enable kivy-ios compilation 195 | include_dirs = [ 196 | environ.get("SDL_INCLUDE_DIR"), 197 | environ.get("FFMPEG_INCLUDE_DIR")] 198 | 199 | elif "NDKPLATFORM" in environ: 200 | # enable python-for-android/py4a compilation 201 | 202 | # ffmpeg: 203 | ffmpeg_lib, ffmpeg_include = get_paths('FFMPEG') 204 | libraries.extend([ 205 | 'avcodec', 'avdevice', 'avfilter', 'avformat', 206 | 'avutil', 'swscale', 'swresample', 'm' 207 | ]) 208 | if c_options['config_postproc']: 209 | libraries.append('postproc') 210 | library_dirs.append(ffmpeg_lib) 211 | include_dirs.append(ffmpeg_include) 212 | 213 | # sdl: 214 | sdl_lib, sdl_include = get_paths('SDL') 215 | if sdl_lib and sdl_include: 216 | libraries.append('SDL2') 217 | library_dirs.append(sdl_lib) 218 | include_dirs.append(sdl_include) 219 | else: # old toolchain 220 | raise ValueError('SDL2 not found') 221 | 222 | # sdl2 mixer: 223 | c_options['use_sdl2_mixer'] = c_options['use_sdl2_mixer'] 224 | if c_options['use_sdl2_mixer']: 225 | _, mixer_include = get_paths('SDL2_MIXER') 226 | libraries.append('SDL2_mixer') 227 | include_dirs.append(mixer_include) 228 | 229 | else: 230 | 231 | # ffmpeg 232 | objects = [ 233 | 'avcodec', 'avdevice', 'avfilter', 'avformat', 234 | 'avutil', 'swscale', 'swresample' 235 | ] 236 | if c_options['config_postproc']: 237 | objects.append('postproc') 238 | for libname in objects[:]: 239 | for key, val in c_options.items(): 240 | if key.endswith(libname) and not val: 241 | objects.remove(libname) 242 | break 243 | 244 | ffmpeg_lib, ffmpeg_include = get_paths('FFMPEG') 245 | flags = {'include_dirs': [], 'library_dirs': [], 'libraries': []} 246 | if ffmpeg_lib is None and ffmpeg_include is None: 247 | flags = pkgconfig(*['lib' + l for l in objects]) 248 | 249 | library_dirs = flags.get('library_dirs', []) if ffmpeg_lib is None \ 250 | else [ffmpeg_lib] 251 | include_dirs = flags.get('include_dirs', []) if ffmpeg_include is None \ 252 | else [ffmpeg_include] 253 | libraries = objects[:] 254 | 255 | # sdl 256 | sdl_lib, sdl_include = get_paths('SDL') 257 | 258 | flags = {} 259 | if sdl_lib is None and sdl_include is None: 260 | flags = pkgconfig('sdl2') 261 | 262 | sdl_libs = flags.get('library_dirs', []) if sdl_lib is None \ 263 | else [sdl_lib] 264 | sdl_includes = flags.get('include_dirs', []) if sdl_include is None \ 265 | else [join(sdl_include, 'SDL2'), sdl_include] 266 | 267 | library_dirs.extend(sdl_libs) 268 | include_dirs.extend(sdl_includes) 269 | libraries.extend(flags.get('libraries', ['SDL2'])) 270 | 271 | c_options['use_sdl2_mixer'] = c_options['use_sdl2_mixer'] 272 | if c_options['use_sdl2_mixer']: 273 | flags = {} 274 | if sdl_lib is None and sdl_include is None: 275 | flags = pkgconfig('SDL2_mixer') 276 | 277 | library_dirs.extend(flags.get('library_dirs', [])) 278 | include_dirs.extend(flags.get('include_dirs', [])) 279 | libraries.extend(flags.get('libraries', ['SDL2_mixer'])) 280 | 281 | 282 | def get_wheel_data(): 283 | data = [] 284 | ff = environ.get('FFMPEG_ROOT') 285 | if ff: 286 | if isdir(join(ff, 'bin')): 287 | data.append(('share/ffpyplayer/ffmpeg/bin', [ 288 | join(ff, 'bin', f) for f in listdir(join(ff, 'bin'))])) 289 | if isdir(join(ff, 'licenses')): 290 | data.append(('share/ffpyplayer/ffmpeg/licenses', [ 291 | join(ff, 'licenses', f) for 292 | f in listdir(join(ff, 'licenses'))])) 293 | if exists(join(ff, 'README.txt')): 294 | data.append(('share/ffpyplayer/ffmpeg', [join(ff, 'README.txt')])) 295 | 296 | sdl = environ.get('SDL_ROOT') 297 | if sdl: 298 | if isdir(join(sdl, 'bin')): 299 | data.append( 300 | ('share/ffpyplayer/sdl/bin', [ 301 | join(sdl, 'bin', f) for f in listdir(join(sdl, 'bin'))])) 302 | return data 303 | 304 | 305 | mods = [ 306 | 'pic', 'threading', 'tools', 'writer', 'player/clock', 'player/core', 307 | 'player/decoder', 'player/frame_queue', 'player/player', 'player/queue'] 308 | c_options['use_sdl2_mixer'] = c_options['use_sdl2_mixer'] 309 | 310 | 311 | if can_use_cython: 312 | mod_suffix = '.pyx' 313 | else: 314 | mod_suffix = '.c' 315 | 316 | print('Generating ffconfig.h') 317 | if not exists(join(src_path, 'ffpyplayer', 'includes')): 318 | mkdir(join(src_path, 'ffpyplayer', 'includes')) 319 | with open(join(src_path, 'ffpyplayer', 'includes', 'ffconfig.h'), 'w') as f: 320 | f.write(''' 321 | #ifndef _FFCONFIG_H 322 | #define _FFCONFIG_H 323 | 324 | #include "SDL_version.h" 325 | #define SDL_VERSIONNUM(X, Y, Z) ((X)*1000 + (Y)*100 + (Z)) 326 | #define SDL_VERSION_ATLEAST(X, Y, Z) (SDL_COMPILEDVERSION >= SDL_VERSIONNUM(X, Y, Z)) 327 | #if defined(__APPLE__) && SDL_VERSION_ATLEAST(1, 2, 14) 328 | #define MAC_REALLOC 1 329 | #else 330 | #define MAC_REALLOC 0 331 | #endif 332 | 333 | #if !defined(_WIN32) && !defined(__APPLE__) 334 | #define NOT_WIN_MAC 1 335 | #else 336 | #define NOT_WIN_MAC 0 337 | #endif 338 | 339 | #if defined(_WIN32) 340 | #define WIN_IS_DEFINED 1 341 | #else 342 | #define WIN_IS_DEFINED 0 343 | #endif 344 | 345 | ''') 346 | for k, v in c_options.items(): 347 | f.write('#define %s %d\n' % (k.upper(), int(v))) 348 | f.write(''' 349 | #endif 350 | ''') 351 | 352 | print('Generating ffconfig.pxi') 353 | with open(join(src_path, 'ffpyplayer', 'includes', 'ffconfig.pxi'), 'w') as f: 354 | for k, v in c_options.items(): 355 | f.write('DEF %s = %d\n' % (k.upper(), int(v))) 356 | 357 | include_dirs.extend( 358 | [join(src_path, 'ffpyplayer'), 359 | join(src_path, 'ffpyplayer', 'includes')]) 360 | print('Include directories: {}'.format(include_dirs)) 361 | print('Library directories: {}'.format(library_dirs)) 362 | ext_modules = [Extension( 363 | 'ffpyplayer.' + src_file.replace('/', '.'), 364 | sources=[join(src_path, 'ffpyplayer', *(src_file + mod_suffix).split('/')), 365 | join(src_path, 'ffpyplayer', 'clib', 'misc.c')], 366 | libraries=libraries, 367 | include_dirs=include_dirs, 368 | library_dirs=library_dirs) 369 | for src_file in mods] 370 | 371 | for e in ext_modules: 372 | e.cython_directives = {"embedsignature": True, 'language_level': 3} 373 | 374 | with open('README.rst') as fh: 375 | long_description = fh.read() 376 | 377 | setup_requires = [] 378 | if declare_cython: 379 | setup_requires.append('cython~=3.0.11') 380 | 381 | setup(name='ffpyplayer', 382 | version=ffpyplayer.__version__, 383 | author='Matthew Einhorn', 384 | author_email='matt@einhorn.dev', 385 | license='LGPL3', 386 | description='A cython implementation of an ffmpeg based player.', 387 | url='https://matham.github.io/ffpyplayer/', 388 | long_description=long_description, 389 | classifiers=[ 390 | 'License :: OSI Approved :: GNU Lesser General Public License v3 ' 391 | '(LGPLv3)', 392 | 'Topic :: Multimedia :: Video', 393 | 'Topic :: Multimedia :: Video :: Display', 394 | 'Topic :: Multimedia :: Sound/Audio :: Players', 395 | 'Topic :: Multimedia :: Sound/Audio :: Players :: MP3', 396 | 'Programming Language :: Python :: 3.7', 397 | 'Programming Language :: Python :: 3.8', 398 | 'Programming Language :: Python :: 3.9', 399 | 'Programming Language :: Python :: 3.10', 400 | 'Programming Language :: Python :: 3.11', 401 | 'Programming Language :: Python :: 3.12', 402 | 'Programming Language :: Python :: 3.13', 403 | 'Operating System :: MacOS :: MacOS X', 404 | 'Operating System :: Microsoft :: Windows', 405 | 'Operating System :: POSIX :: BSD :: FreeBSD', 406 | 'Operating System :: POSIX :: Linux', 407 | 'Intended Audience :: Developers'], 408 | packages=['ffpyplayer', 'ffpyplayer.player', 'ffpyplayer.tests'], 409 | package_data={ 410 | 'ffpyplayer': [ 411 | 'player/*.pxd', 'clib/misc.h', 'includes/*.pxi', 'includes/*.h', 412 | '*.pxd', 'player/*.pyx', 'clib/misc.c', '*.pyx']}, 413 | data_files=get_wheel_data(), 414 | cmdclass=cmdclass, ext_modules=ext_modules, 415 | setup_requires=setup_requires) 416 | --------------------------------------------------------------------------------