├── .gitignore ├── Dockerfile ├── Pipfile ├── Pipfile.lock ├── README.md ├── build.sh ├── docs └── img │ └── github-banner.png ├── examples ├── input.mp4 └── player.html ├── src ├── IPFSStreamingVideo │ ├── __init__.py │ └── ipfs_streaming_video.py └── cli.py └── wrapper.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/**/usage.statistics.xml 10 | .idea/**/dictionaries 11 | .idea/**/shelf 12 | 13 | # Sensitive or high-churn files 14 | .idea/**/dataSources/ 15 | .idea/**/dataSources.ids 16 | .idea/**/dataSources.local.xml 17 | .idea/**/sqlDataSources.xml 18 | .idea/**/dynamic.xml 19 | .idea/**/uiDesigner.xml 20 | .idea/**/dbnavigator.xml 21 | 22 | # Gradle 23 | .idea/**/gradle.xml 24 | .idea/**/libraries 25 | 26 | # Gradle and Maven with auto-import 27 | # When using Gradle or Maven with auto-import, you should exclude module files, 28 | # since they will be recreated, and may cause churn. Uncomment if using 29 | # auto-import. 30 | # .idea/modules.xml 31 | # .idea/*.iml 32 | # .idea/modules 33 | 34 | # CMake 35 | cmake-build-*/ 36 | 37 | # Mongo Explorer plugin 38 | .idea/**/mongoSettings.xml 39 | 40 | # File-based project format 41 | *.iws 42 | 43 | # IntelliJ 44 | out/ 45 | 46 | # mpeltonen/sbt-idea plugin 47 | .idea_modules/ 48 | 49 | # JIRA plugin 50 | atlassian-ide-plugin.xml 51 | 52 | # Cursive Clojure plugin 53 | .idea/replstate.xml 54 | 55 | # Crashlytics plugin (for Android Studio and IntelliJ) 56 | com_crashlytics_export_strings.xml 57 | crashlytics.properties 58 | crashlytics-build.properties 59 | fabric.properties 60 | 61 | # Editor-based Rest Client 62 | .idea/httpRequests 63 | ### Python template 64 | # Byte-compiled / optimized / DLL files 65 | __pycache__/ 66 | *.py[cod] 67 | *$py.class 68 | 69 | # C extensions 70 | *.so 71 | 72 | # Distribution / packaging 73 | .Python 74 | build/ 75 | develop-eggs/ 76 | dist/ 77 | downloads/ 78 | eggs/ 79 | .eggs/ 80 | lib/ 81 | lib64/ 82 | parts/ 83 | sdist/ 84 | var/ 85 | wheels/ 86 | *.egg-info/ 87 | .installed.cfg 88 | *.egg 89 | MANIFEST 90 | 91 | # PyInstaller 92 | # Usually these files are written by a python script from a template 93 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 94 | *.manifest 95 | *.spec 96 | 97 | # Installer logs 98 | pip-log.txt 99 | pip-delete-this-directory.txt 100 | 101 | # Unit test / coverage reports 102 | htmlcov/ 103 | .tox/ 104 | .coverage 105 | .coverage.* 106 | .cache 107 | nosetests.xml 108 | coverage.xml 109 | *.cover 110 | .hypothesis/ 111 | .pytest_cache/ 112 | 113 | # Translations 114 | *.mo 115 | *.pot 116 | 117 | # Django stuff: 118 | *.log 119 | local_settings.py 120 | db.sqlite3 121 | 122 | # Flask stuff: 123 | instance/ 124 | .webassets-cache 125 | 126 | # Scrapy stuff: 127 | .scrapy 128 | 129 | # Sphinx documentation 130 | docs/_build/ 131 | 132 | # PyBuilder 133 | target/ 134 | 135 | # Jupyter Notebook 136 | .ipynb_checkpoints 137 | 138 | # pyenv 139 | .python-version 140 | 141 | # celery beat schedule file 142 | celerybeat-schedule 143 | 144 | # SageMath parsed files 145 | *.sage.py 146 | 147 | # Environments 148 | .env 149 | .venv 150 | env/ 151 | venv/ 152 | ENV/ 153 | env.bak/ 154 | venv.bak/ 155 | 156 | # Spyder project settings 157 | .spyderproject 158 | .spyproject 159 | 160 | # Rope project settings 161 | .ropeproject 162 | 163 | # mkdocs documentation 164 | /site 165 | 166 | # mypy 167 | .mypy_cache/ 168 | 169 | # Custom 170 | .idea 171 | /output/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:alpine 2 | 3 | ENV APK_PACKAGES \ 4 | alpine-sdk \ 5 | libffi-dev \ 6 | tzdata \ 7 | ffmpeg 8 | 9 | ENV PIP_NO_CACHE_DIR false 10 | 11 | RUN apk --no-cache add $APK_PACKAGES 12 | 13 | RUN pip --no-cache-dir install pipenv 14 | 15 | RUN cp /usr/share/zoneinfo/Europe/London /etc/localtime && \ 16 | echo "Europe/London" > /etc/timezone && \ 17 | apk del tzdata 18 | 19 | RUN addgroup -S project && \ 20 | adduser -S project -G project 21 | 22 | RUN echo "project ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/project && \ 23 | chmod 0440 /etc/sudoers.d/project 24 | 25 | WORKDIR /home/project 26 | 27 | COPY src . 28 | COPY Pipfile . 29 | COPY Pipfile.lock . 30 | 31 | RUN pipenv install --system --deploy --ignore-pipfile 32 | 33 | RUN python -m compileall -b .; \ 34 | find . -name "*.py" -type f -print -delete 35 | 36 | USER project 37 | 38 | ENTRYPOINT ["python", "cli.pyc"] -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | ipfshttpclient = "*" 10 | ffmpeg-python = "*" 11 | 12 | [requires] 13 | python_version = "3.7" 14 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "5e84c1c9eb08ae9b635d0015e3b457059239e1ec8dff5e9348f5728df5873373" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "base58": { 20 | "hashes": [ 21 | "sha256:11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2", 22 | "sha256:c5d0cb3f5b6e81e8e35da5754388ddcc6d0d14b6c6a132cb93d69ed580a7278c" 23 | ], 24 | "markers": "python_version >= '3.5'", 25 | "version": "==2.1.1" 26 | }, 27 | "certifi": { 28 | "hashes": [ 29 | "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", 30 | "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" 31 | ], 32 | "index": "pypi", 33 | "version": "==2022.12.7" 34 | }, 35 | "charset-normalizer": { 36 | "hashes": [ 37 | "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", 38 | "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" 39 | ], 40 | "markers": "python_full_version >= '3.6.0'", 41 | "version": "==2.1.1" 42 | }, 43 | "ffmpeg-python": { 44 | "hashes": [ 45 | "sha256:57e3295200f853c1eb9bcf35648debfb14504b2fd061a36709729dfacfb54a65", 46 | "sha256:975225f9a28e1e728c1950156e430facc5c97d33beb1ef874cf77fee43b2f1fd" 47 | ], 48 | "index": "pypi", 49 | "version": "==0.1.18" 50 | }, 51 | "future": { 52 | "hashes": [ 53 | "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" 54 | ], 55 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 56 | "version": "==0.18.2" 57 | }, 58 | "idna": { 59 | "hashes": [ 60 | "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", 61 | "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" 62 | ], 63 | "markers": "python_version >= '3.5'", 64 | "version": "==3.4" 65 | }, 66 | "ipfshttpclient": { 67 | "hashes": [ 68 | "sha256:0a199a1005fe44bff9da28b5af4785b0b09ca700baac9d1e26718fe23fe89bb7", 69 | "sha256:bee95c500edf669bb8a984d5588fc133fda9ec67845c5688bcbbea030a03f10f" 70 | ], 71 | "index": "pypi", 72 | "version": "==0.4.12" 73 | }, 74 | "multiaddr": { 75 | "hashes": [ 76 | "sha256:30b2695189edc3d5b90f1c303abb8f02d963a3a4edf2e7178b975eb417ab0ecf", 77 | "sha256:5c0f862cbcf19aada2a899f80ef896ddb2e85614e0c8f04dd287c06c69dac95b" 78 | ], 79 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 80 | "version": "==0.0.9" 81 | }, 82 | "netaddr": { 83 | "hashes": [ 84 | "sha256:9666d0232c32d2656e5e5f8d735f58fd6c7457ce52fc21c98d45f2af78f990ac", 85 | "sha256:d6cc57c7a07b1d9d2e917aa8b36ae8ce61c35ba3fcd1b83ca31c5a0ee2b5a243" 86 | ], 87 | "version": "==0.8.0" 88 | }, 89 | "requests": { 90 | "hashes": [ 91 | "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", 92 | "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" 93 | ], 94 | "markers": "python_version >= '3.7' and python_version < '4'", 95 | "version": "==2.28.1" 96 | }, 97 | "six": { 98 | "hashes": [ 99 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 100 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 101 | ], 102 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 103 | "version": "==1.16.0" 104 | }, 105 | "urllib3": { 106 | "hashes": [ 107 | "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc", 108 | "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8" 109 | ], 110 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 111 | "version": "==1.26.13" 112 | }, 113 | "varint": { 114 | "hashes": [ 115 | "sha256:a6ecc02377ac5ee9d65a6a8ad45c9ff1dac8ccee19400a5950fb51d594214ca5" 116 | ], 117 | "version": "==1.0.2" 118 | } 119 | }, 120 | "develop": {} 121 | } 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![banner](https://raw.githubusercontent.com/desiredState/IPFSStreamingVideo/master/docs/img/github-banner.png "IPFS Streaming Video") 2 | 3 | Converts an input video file to HLS at multiple qualities, optionally adding output chunks and amended m3u8 playlists to IPFS. 4 | 5 | ## Usage 6 | 7 | ### Command-Line Interface 8 | 9 | ````bash 10 | python3 src/cli.py --help 11 | ```` 12 | 13 | For example: 14 | 15 | ```bash 16 | python3 src/cli.py --ipfs --input-file examples/input.mp4 17 | ``` 18 | 19 | ### Python Module 20 | 21 | An example of `IPFSStreamingVideo()` Python module usage can be seen in `src/cli.py` 22 | 23 | ### Web Player 24 | 25 | An example HLS-capable web player can be found at `examples/player.html`, which can also be added to IPFS! Just change the m3u8 playlist hash in `player.html` to the one returned by the CLI. 26 | 27 | ## Contributing 28 | 29 | All contributions are welcome, just open a Pull Request or Issue. 30 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | function usage { 6 | cat </dev/null; then 25 | echo -e "${RED}BUILD > "${i}" is required. Please install it then try again.${NONE}" 26 | exit 1 27 | fi 28 | done 29 | } 30 | 31 | function build { 32 | # Build Docker Image. 33 | echo -e "${MAGENTA}BUILD > Building the ${NAMESPACE}/${IMAGE}:${TAG} Docker Image...${NONE}" 34 | docker build -t "${NAMESPACE}/${IMAGE}:${TAG}" . 35 | echo -e "${GREEN}BUILD > OK.${NONE}" 36 | } 37 | 38 | function push { 39 | # Push Docker Image. 40 | echo -e "${MAGENTA}BUILD > Pushing the ${NAMESPACE}/${IMAGE}:${TAG} Docker Image...${NONE}" 41 | docker push "${NAMESPACE}/${IMAGE}:${TAG}" 42 | echo -e "${GREEN}BUILD > OK.${NONE}" 43 | } 44 | 45 | # Environment variable overrides. 46 | export NAMESPACE=${NAMESPACE:='desiredstate'} 47 | export IMAGE=${IMAGE:='ipfsstreamingvideo'} 48 | export TAG=${VERSION:='latest'} 49 | export UPDATE=${UPDATE:=false} # false as we're building the Image locally. 50 | 51 | MAGENTA=$(tput setaf 5) 52 | GREEN=$(tput setaf 2) 53 | RED=$(tput setaf 1) 54 | NONE=$(tput sgr 0) 55 | 56 | check_deps 57 | 58 | case $1 in 59 | build) 60 | build 61 | ;; 62 | push) 63 | push 64 | ;; 65 | *) 66 | usage 67 | exit 1 68 | esac 69 | 70 | echo -e "${GREEN}BUILD > Finished.${NONE}" 71 | -------------------------------------------------------------------------------- /docs/img/github-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/desiredState/IPFSStreamingVideo/f955ef40507c31cf46277b1db8706a04f61e4ad9/docs/img/github-banner.png -------------------------------------------------------------------------------- /examples/input.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/desiredState/IPFSStreamingVideo/f955ef40507c31cf46277b1db8706a04f61e4ad9/examples/input.mp4 -------------------------------------------------------------------------------- /examples/player.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |

IPFS Streaming Video example

11 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/IPFSStreamingVideo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/desiredState/IPFSStreamingVideo/f955ef40507c31cf46277b1db8706a04f61e4ad9/src/IPFSStreamingVideo/__init__.py -------------------------------------------------------------------------------- /src/IPFSStreamingVideo/ipfs_streaming_video.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import os 3 | 4 | import ffmpeg 5 | import ipfshttpclient 6 | 7 | 8 | class IPFSStreamingVideo(object): 9 | def __init__(self): 10 | self.renditions = [ 11 | # {'name': '240p', 'resolution': '426x240', 'bitrate': '400k', 'audiorate': '64k'}, 12 | # {'name': '360p', 'resolution': '640x360', 'bitrate': '700k', 'audiorate': '96k'}, 13 | # {'name': '480p', 'resolution': '854x480', 'bitrate': '1250k', 'audiorate': '128k'}, 14 | {'name': 'HD 720p', 'resolution': '1280x720', 'bitrate': '2500k', 'audiorate': '128k'}, 15 | # {'name': 'HD 720p 60fps', 'resolution': '1280x720', 'bitrate': '3500k', 'audiorate': '128k'}, 16 | {'name': 'Full HD 1080p', 'resolution': '1920x1080', 'bitrate': '4500k', 'audiorate': '192k'}, 17 | # {'name': 'Full HD 1080p 60fps', 'resolution': '1920x1080', 'bitrate': '5800k', 'audiorate': '192k'}, 18 | # {'name': '4k', 'resolution': '3840x2160', 'bitrate': '14000k', 'audiorate': '192k'}, 19 | # {'name': '4k 60fps', 'resolution': '3840x2160', 'bitrate': '23000k', 'audiorate': '192k'} 20 | ] 21 | 22 | def convert_to_hls(self, input_file, segment_format='%03d.ts', output_dir='output'): 23 | ffmpeg_input_stream = ffmpeg.input(input_file) 24 | ffmpeg_output_streams = [] 25 | 26 | if not os.path.exists(output_dir): 27 | os.makedirs(output_dir) 28 | 29 | for rendition in self.renditions: 30 | ffmpeg_params = { 31 | 'vf': "scale=w={}:h={}:force_original_aspect_ratio=decrease".format( 32 | rendition['resolution'].split('x')[0], rendition['resolution'].split('x')[1]), 33 | 'c:a': 'aac', 34 | 'ar': '48000', 35 | 'c:v': 'h264', 36 | 'profile:v': 'main', 37 | 'crf': '20', 38 | 'sc_threshold': '0', 39 | 'g': '48', 40 | 'keyint_min': '48', 41 | 'hls_time': '4', 42 | 'hls_playlist_type': 'vod', 43 | 'b:v': f"{rendition['bitrate']}", 44 | 'maxrate': '856k', 45 | 'bufsize': '1200k', 46 | 'b:a': f"{rendition['audiorate']}", 47 | 'hls_segment_filename': f"{output_dir}/{rendition['resolution'].split('x')[1]}p_{segment_format}" 48 | } 49 | 50 | ffmpeg_output_streams.append( 51 | ffmpeg.output( 52 | ffmpeg_input_stream, 53 | f"{output_dir}/{rendition['resolution'].split('x')[1]}p.m3u8", 54 | **ffmpeg_params 55 | ) 56 | ) 57 | 58 | output_streams = ffmpeg.merge_outputs(*ffmpeg_output_streams) 59 | ffmpeg.run(output_streams) 60 | 61 | def ipfs_add_dir(self, directory, pattern, recursive=True): 62 | with ipfshttpclient.connect() as ipfs_client: 63 | response = ipfs_client.add(directory, pattern=pattern, recursive=recursive) 64 | 65 | return response 66 | 67 | def ipfs_add_file(self, file_path): 68 | with ipfshttpclient.connect() as ipfs_client: 69 | response = ipfs_client.add(file_path) 70 | 71 | return response 72 | 73 | def rewrite_m3u8_files(self, ipfs_add_response): 74 | for item in ipfs_add_response: 75 | if str(item['Name']).endswith('.ts'): 76 | with fileinput.FileInput(f"{str(item['Name']).split('_')[0]}.m3u8", inplace=True) as file: 77 | for line in file: 78 | print(line.replace(str(item['Name']).split('/')[-1], 79 | f"http://127.0.0.1:8080/ipfs/{item['Hash']}"), end='') 80 | 81 | def generate_master_m3u8(self, output_dir, filename, ipfs_hashes): 82 | m3u8_content = '#EXTM3U\n#EXT-X-VERSION:3' 83 | 84 | for rendition in self.renditions: 85 | for record in ipfs_hashes: 86 | if str(record['Name']).split('/')[-1].replace('p.m3u8', '') == rendition['resolution'].split('x')[1]: 87 | hash = record['Hash'] 88 | continue 89 | 90 | m3u8_content += f"\n#EXT-X-STREAM-INF:BANDWIDTH={str(rendition['bitrate']).replace('k', '000')}," \ 91 | f"RESOLUTION={rendition['resolution']}\nhttp://127.0.0.1:8080/ipfs/{hash}" 92 | 93 | with open(f'{output_dir}/{filename}', "w") as file: 94 | file.write(m3u8_content) 95 | 96 | def preload_ipfs_gateway_caches(self): 97 | # Request every hash from IPFS gateways. 98 | pass 99 | -------------------------------------------------------------------------------- /src/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | 5 | from IPFSStreamingVideo.ipfs_streaming_video import IPFSStreamingVideo 6 | 7 | parser = argparse.ArgumentParser(description='IPFS Streaming Video') 8 | parser.add_argument('-f', '--input-file', help='path of the input file', required=True) 9 | parser.add_argument('-s', '--segment-format', default='%03d.ts', help='filename format of generated .ts files') 10 | parser.add_argument('-o', '--output-dir', default='output', help='path of the output directory') 11 | parser.add_argument('-m', '--master-filename', default='master.m3u8', help='name of the master m3u8 file') 12 | parser.add_argument('-i', '--ipfs', action='store_true', help='add output files to IPFS and amend m3u8 files') 13 | args = parser.parse_args() 14 | 15 | ipfs_streaming_video = IPFSStreamingVideo() 16 | 17 | # Convert the input file to HLS chunks (.ts files) 18 | # TODO Determine input quality and select correct renditions. 19 | ipfs_streaming_video.convert_to_hls( 20 | input_file=args.input_file, segment_format=args.segment_format, output_dir=args.output_dir 21 | ) 22 | 23 | if args.ipfs: 24 | # Add the chunks to IPFS. 25 | ipfs_add_response_ts = ipfs_streaming_video.ipfs_add_dir(directory=args.output_dir, pattern='*.ts') 26 | # pprint.pprint(ipfs_add_response_ts) 27 | 28 | # Rewrite m3u8 playlists. 29 | ipfs_streaming_video.rewrite_m3u8_files(ipfs_add_response_ts) 30 | 31 | # Add the rewritten m3u8 playlists to IPFS. 32 | ipfs_add_response_m3u8 = ipfs_streaming_video.ipfs_add_dir(directory=args.output_dir, pattern='*.m3u8') 33 | # pprint.pprint(ipfs_add_response_m3u8) 34 | 35 | # Generate master m3u8 playlist. 36 | ipfs_streaming_video.generate_master_m3u8( 37 | output_dir=args.output_dir, 38 | filename=args.master_filename, 39 | ipfs_hashes=ipfs_add_response_m3u8 40 | ) 41 | 42 | # Add m3u8 master playlists to IPFS. 43 | ipfs_add_response_master_m3u8 = ipfs_streaming_video.ipfs_add_file( 44 | file_path=f'{args.output_dir}/{args.master_filename}' 45 | ) 46 | 47 | print(f"Master playlist added to IPFS as http://127.0.0.1:8080/ipfs/{ipfs_add_response_master_m3u8['Hash']}") 48 | -------------------------------------------------------------------------------- /wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | NAMESPACE=${NAMESPACE:='desiredstate'} 4 | IMAGE=${IMAGE:='ipfsstreamingvideo'} 5 | TAG=${VERSION:='latest'} 6 | UPDATE=${UPDATE:=true} 7 | 8 | if ! hash docker &>/dev/null; then 9 | echo 'Docker is required to run IPFSStreamingVideo. Please install it then try again.' 10 | exit 1 11 | fi 12 | 13 | if [[ "$UPDATE" = true ]] ; then 14 | docker pull "${NAMESPACE}/${IMAGE}:${TAG}" 15 | fi 16 | 17 | docker run -ti --rm -v $(pwd):/home/project "${NAMESPACE}/${IMAGE}:${TAG}" "${@}" 18 | --------------------------------------------------------------------------------