├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── install.py ├── make_compile.py ├── make_universal.py └── package.py /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Project 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - master 7 | paths-ignore: 8 | - '**.md' 9 | tags: 10 | - '*' 11 | pull_request: 12 | paths-ignore: 13 | - '**.md' 14 | jobs: 15 | build: 16 | name: Build-FFmpeg-${{ matrix.tag }} 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | include: 22 | - os: macos-11 23 | tag: n5.0.1 24 | steps: 25 | - name: CleanOldArtifact 26 | uses: kolpav/purge-artifacts-action@v1 27 | with: 28 | token: ${{ secrets.GITHUB_TOKEN }} 29 | expire-in: 15days 30 | 31 | - name: Install Dependence 32 | id: install-dependence 33 | shell: pwsh 34 | run: | 35 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 36 | brew install nasm 37 | 38 | - name: CheckOut 39 | id: checkout 40 | uses: actions/checkout@v3 41 | 42 | - name: Clone FFmpeg 43 | uses: actions/checkout@v3 44 | with: 45 | repository: 'FFmpeg/FFmpeg' 46 | ref: ${{ matrix.tag }} 47 | path: 'ffmpeg' 48 | 49 | - name: Build project 50 | id: build 51 | shell: pwsh 52 | run: | 53 | python3 make_compile.py --ffmpeg_dir ffmpeg --target_dir ffmpeg 54 | python3 make_universal.py --dir ffmpeg 55 | python3 package.py --dir ffmpeg --tag ${{ matrix.tag }} 56 | 57 | - uses: actions/upload-artifact@v2 58 | with: 59 | name: FFmpeg-${{ matrix.tag }} 60 | path: ${{ github.workspace }}/*.zip 61 | 62 | - name: Upload release 63 | uses: softprops/action-gh-release@v1 64 | if: startsWith(github.ref, 'refs/tags/') 65 | with: 66 | files: | 67 | ${{ github.workspace }}/*.zip 68 | ${{ github.workspace }}/install.py -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .idea/ 132 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ColorsWind 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FFmpeg-macOS 2 | FFmpeg shared universal binaries (x86_64 and arm64) for macOS. 3 | 4 | ## Usage 5 | 6 | You may compile it by yourself or download and install prebuild binary. 7 | 8 | - **Compile FFmpeg by yourself.** 9 | 10 | By default, our script will compile FFmpeg with `--enable-shared --disable-static`, if you want to use custom flags, you can edit `make_compile.py`. 11 | 12 | Available options: `--ffmpeg_dir --target_dir --dir`, use `-h` to see help. 13 | 14 | ```bash 15 | git clone git@github.com:ColorsWind/FFmpeg-macOS.git build-script 16 | git clone git@github.com:FFmpeg/FFmpeg.git ffmpeg 17 | cd ffmpeg 18 | git checkout n5.0.1 19 | python ../build-script/make_compile.py 20 | python ../build-script/make_universal.py 21 | python ../build-script/package.py --tag n5.0.1 22 | ``` 23 | 24 | - **Install Prebuilt Binary** 25 | 26 | You may download from [release](https://github.com/ColorsWind/FFmpeg-macOS/releases/) and install it by `install.py`. 27 | 28 | ```bash 29 | wget https://github.com/ColorsWind/FFmpeg-macOS/releases/download/n5.0.1-patch3/FFmpeg-shared-n5.0.1-OSX-universal.zip 30 | wget https://github.com/ColorsWind/FFmpeg-macOS/releases/download/n5.0.1-patch3/install.py 31 | python install.py FFmpeg-shared-n5.0.1-OSX-universal.zip ~/ffmpeg-n5.0.1 32 | ``` 33 | Then, FFmpeg-shared will be installed on `~/ffmpeg-n5.0.1`. 34 | 35 | 36 | ## Custom 37 | 38 | If you want to make changed compile parameters, (e.g. add decoders support or build static library), here is a description about those scripts. 39 | 40 | `make_compile.py` configure and cross compile FFmpeg. 41 | `make_universal.py` use `lipo` to merge x86_64 and ARM64 binary, and then use `install_name_tool` to fix library load path. 42 | `install.py` unzip and use `install_name_tool` to fix library load path. 43 | 44 | 45 | 46 | ## Release 47 | 48 | Latest: tag n5.0.1 49 | https://github.com/ColorsWind/FFmpeg-macOS/releases 50 | 51 | ## License 52 | 53 | [FFmpeg](http://ffmpeg.org) licensed under the [LGPLv2](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html). and its source can be downloaded [here](https://github.com/FFmpeg/FFmpeg). 54 | 55 | This repository contains FFmpeg build scripts, which licensed under the MIT License. 56 | 57 | MIT License 58 | 59 | Copyright (c) 2022 ColorsWind 60 | 61 | Permission is hereby granted, free of charge, to any person obtaining a copy 62 | of this software and associated documentation files (the "Software"), to deal 63 | in the Software without restriction, including without limitation the rights 64 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 65 | copies of the Software, and to permit persons to whom the Software is 66 | furnished to do so, subject to the following conditions: 67 | 68 | The above copyright notice and this permission notice shall be included in all 69 | copies or substantial portions of the Software. 70 | 71 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 72 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 73 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 74 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 75 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 76 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 77 | SOFTWARE. 78 | -------------------------------------------------------------------------------- /install.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import pathlib 3 | import sys 4 | import os 5 | from argparse import ArgumentParser 6 | 7 | 8 | def execute(command): 9 | print(command, file=sys.stderr) 10 | os.system(command) 11 | 12 | 13 | def rename_dylib_reference(base_dir: pathlib.Path, file: pathlib.Path): 14 | with os.popen(f"otool -L {file}") as p: 15 | query_out = p.readlines() 16 | execute(f"install_name_tool {file} -id {file}") 17 | for line in query_out: 18 | line = line.strip().split(" ")[0] 19 | for prefix in ["install_arm64", "install_x86_64", "install_universal"]: 20 | begin = line.find(prefix) 21 | if begin < 0: 22 | continue 23 | execute(f"install_name_tool {file} -change {line} {(base_dir / line[begin + len(prefix) + 1:]).absolute()}") 24 | break 25 | 26 | 27 | if __name__ == "__main__": 28 | parser = ArgumentParser(description="Package the generated files into ZIP.") 29 | parser.add_argument("ffmpeg_path", type=str, help='indicate FFmpeg dir or archive.') 30 | parser.add_argument("target_dir", type=str, help='indicate target.') 31 | args = parser.parse_args() 32 | ffmpeg_path = pathlib.Path(args.ffmpeg_path) 33 | target_dir = pathlib.Path(args.target_dir) 34 | if ffmpeg_path.is_file(): 35 | shutil.unpack_archive(ffmpeg_path, target_dir) 36 | else: 37 | shutil.copytree(ffmpeg_path, target_dir) 38 | for f in target_dir.rglob("*"): 39 | 40 | if f.is_file() and (f.suffix in {'.dylib', '.a'} or len(f.suffix) == 0 and f.parent.absolute().name == "bin"): 41 | rename_dylib_reference(target_dir, f) 42 | -------------------------------------------------------------------------------- /make_compile.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | from multiprocessing import cpu_count 3 | import pathlib 4 | import sys 5 | import os 6 | 7 | 8 | def execute(command: str): 9 | print(f"Execute: {command}.") 10 | os.system(command) 11 | 12 | 13 | if __name__ == "__main__": 14 | parser = ArgumentParser(description="Configure & Make & Install FFmpeg.") 15 | parser.add_argument("--ffmpeg_dir", type=str, default=os.getcwd(), help='indicate FFmpeg dir.') 16 | parser.add_argument("--target_dir", type=str, default=os.getcwd(), help='indicate target dir.') 17 | args = parser.parse_args() 18 | ffmpeg_dir = pathlib.Path(args.ffmpeg_dir).absolute() 19 | target_dir = pathlib.Path(args.target_dir).absolute() 20 | print(f"Compile... {ffmpeg_dir}") 21 | 22 | 23 | def clean(): 24 | print("Clean project.") 25 | execute(f"cd {ffmpeg_dir} && make clean && make distclean") 26 | 27 | 28 | def make(arch: str): 29 | n_cpu = cpu_count() 30 | print("Configure project.") 31 | execute( 32 | f"cd {ffmpeg_dir} && ./configure --enable-cross-compile --prefix={target_dir / ('install_' + arch + '/')} " 33 | f"--enable-shared --disable-static --arch={arch} --cc='clang -arch {arch}'" 34 | ) 35 | print(f"Make project ({n_cpu} threads).") 36 | execute(f"cd {ffmpeg_dir} && make -j{n_cpu}") 37 | print(f"Install project.") 38 | execute(f"cd {ffmpeg_dir} && make install") 39 | 40 | 41 | print("----------arm64----------") 42 | clean() 43 | make("arm64") 44 | print("----------x86_64----------") 45 | clean() 46 | make("x86_64") 47 | -------------------------------------------------------------------------------- /make_universal.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | import shutil 3 | import sys 4 | import os 5 | import pathlib 6 | 7 | 8 | def execute(command): 9 | print(command, file=sys.stderr) 10 | os.system(command) 11 | 12 | 13 | def create_universal_binary(x86_path, arm_path, universal_path): 14 | execute(f"lipo -create -arch arm64 {arm_path} -arch x86_64 {x86_path} -output {universal_path}") 15 | 16 | with os.popen(f"otool -L {arm_path}") as p: 17 | query_out = p.readlines() 18 | execute(f"install_name_tool {universal_path} -id {universal_path}") 19 | for line in query_out: 20 | line = line.strip().split(" ")[0] 21 | if "install_arm64" in line: 22 | execute( 23 | f"install_name_tool {universal_path} " 24 | f"-change {line} {line.replace('install_arm64', 'install_universal')}" 25 | ) 26 | 27 | with os.popen(f"otool -L {x86_path}") as p: 28 | query_out = p.readlines() 29 | for line in query_out: 30 | line = line.strip().split(" ")[0] 31 | if "install_x86_64" in line: 32 | execute( 33 | f"install_name_tool {universal_path} " 34 | f"-change {line} {line.replace('install_x86_64', 'install_universal')}" 35 | ) 36 | 37 | 38 | if __name__ == "__main__": 39 | parser = ArgumentParser(description="Use lipo tool to turn into universal binaries.") 40 | parser.add_argument("--dir", type=str, default=os.getcwd(), help='indicate target dir.') 41 | args = parser.parse_args() 42 | target_dir = pathlib.Path(args.dir).absolute() 43 | print(f"Make universal binaries... {target_dir}") 44 | 45 | install_intel_dir = target_dir / "install_x86_64" 46 | install_apple_dir = target_dir / "install_arm64" 47 | install_universal_dir = target_dir / "install_universal" 48 | if install_universal_dir.exists(): 49 | shutil.rmtree(install_universal_dir) 50 | install_universal_dir.mkdir() 51 | 52 | for f in install_apple_dir.rglob("*"): 53 | relative = f.relative_to(install_apple_dir) 54 | target = install_universal_dir / relative 55 | if f.is_dir(): 56 | target = install_universal_dir / relative 57 | target.mkdir(exist_ok=True, parents=True) 58 | else: 59 | if f.is_symlink(): 60 | real = f.resolve() 61 | os.symlink(real.name, target) 62 | elif target.suffix in {'.dylib', '.a'} or len(target.suffix) == 0 and os.access(f, os.X_OK): 63 | create_universal_binary(install_intel_dir / relative, install_apple_dir / relative, target) 64 | else: 65 | shutil.copy2(f, target, follow_symlinks=False) 66 | -------------------------------------------------------------------------------- /package.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | import os 3 | import pathlib 4 | import shutil 5 | 6 | 7 | if __name__ == "__main__": 8 | parser = ArgumentParser(description="Package the generated files into ZIP.") 9 | parser.add_argument("--dir", type=str, default=os.getcwd(), help='indicate target dir.') 10 | parser.add_argument("--tag", type=str, default="UNKNOWN", help='indicate FFmpeg tag.') 11 | args = parser.parse_args() 12 | target_dir = pathlib.Path(args.dir).absolute() 13 | tag = args.tag 14 | print(f"Packaging... {target_dir} {tag}") 15 | install_intel_dir = target_dir / "install_x86_64" 16 | install_apple_dir = target_dir / "install_arm64" 17 | install_universal_dir = target_dir / "install_universal" 18 | 19 | shutil.make_archive(f"FFmpeg-shared-{tag}-OSX-arm64", "zip", install_apple_dir) 20 | print("Finished arm64.") 21 | shutil.make_archive(f"FFmpeg_shared-{tag}-OSX-x86_64", "zip", install_intel_dir) 22 | print("Finished x86_64.") 23 | shutil.make_archive(f"FFmpeg-shared-{tag}-OSX-universal", "zip", install_universal_dir) 24 | print("Finished universal.") 25 | --------------------------------------------------------------------------------