├── .gitignore ├── .readthedocs.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── docs ├── Makefile ├── make.bat ├── requirements.txt └── source │ ├── conf.py │ ├── examples.rst │ ├── history.rst │ ├── howto.rst │ └── index.rst ├── fvid ├── __init__.py ├── __main__.py ├── cythonizer.py ├── fvid.py ├── fvid_cython.cpp └── fvid_cython.pyx ├── requirements.txt ├── setup.py └── tests ├── decoded ├── README.txt ├── large_pdf.pdf.REMOVED.git-id └── rio_de_janeiro_skyline.jpg ├── encoded ├── fvid -dyz5.mp4.REMOVED.git-id ├── yt fvid -dy.mp4.REMOVED.git-id └── yt fvid -dyz5.mp4.REMOVED.git-id ├── encoding_test.py ├── helper.py ├── requirements.txt └── temp └── yt_fvid_-ey5.mp4 /.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 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # Ignore output files 107 | fvid_frames/ 108 | 109 | # Theel does her testing inside the fvid directory 110 | fvid/*.mp4 111 | fvid/*.m4a 112 | fvid/*.txt 113 | fvid/*.jpg 114 | fvid/*.png 115 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | python: 2 | version: 3 3 | extra_requirements: 4 | - docs 5 | pip_install: true -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | __Planned (1.1.0)__ 2 | - GUI 3 | - Use json-encoded strings instead of pickle for storing stuff 4 | - Add `-5` flag for H265 encoding/decoding, much smaller output file 5 | - Add zfec error correction 6 | 7 | __1.0.0__ 8 | 9 | Additions: 10 | - Added support for passwords, custom framerates, and Cython compilation 11 | - file.mp4 compression with ``gzip`` 12 | - Pickled data to allow decompression with original file name 13 | 14 | Bug Fixes: 15 | - Fixed ``file.bin`` bug 16 | - Removed ``magic``, ``mime`` 17 | - New ``make_image_sequence`` logic 18 | - Framerate patch 19 | 20 | Performance: 21 | - Huge Cython/Python speedups 22 | 23 | __0.0.2__ 24 | - Bug fixes 25 | - Minor speedups 26 | 27 | __0.0.1__ 28 | - Initial release 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alfredo Sequeida 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 | [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://GitHub.com/Naereen/StrapDown.js/graphs/commit-activity) 2 | [![PyPI license](https://img.shields.io/pypi/l/ansicolortags.svg)](https://pypi.python.org/pypi/ansicolortags/) 3 | 4 | [Demonstration/Explanation Video](https://youtu.be/yu_ZIr0q5rU) 5 | 6 | fvid is a project that aims to encode any file as a video using 1-bit color images 7 | to survive compression algorithms for data retrieval. 8 | 9 |

10 | fvid 11 |
12 |

13 | 14 | --- 15 | 16 | # Installation 17 | 18 | Requires installation of [FFmpeg](https://ffmpeg.org/download.html) and libmagic first, then install using pip3 19 | 20 | Linux/macOS 21 | 22 | ``` 23 | pip3 install fvid 24 | ``` 25 | 26 | Windows 27 | 28 | ``` 29 | py -m pip install fvid 30 | ``` 31 | 32 | # Usage 33 | 34 | Encoding files as videos 35 | 36 | Linux/OSX 37 | 38 | ``` 39 | fvid -i [input file] -e 40 | fvid -i [input file] --framerate 1 -e 41 | fvid -i [input file] --password "wow fvid is cool" -e 42 | ``` 43 | 44 | Windows 45 | 46 | ``` 47 | py -m fvid -i [input file] -e 48 | py -m fvid -i [input file] --framerate 1 -e 49 | py -m fvid -i [input file] --password "wow fvid is cool" -e 50 | ``` 51 | 52 | Retrieving data from videos 53 | 54 | Linux/OSX 55 | 56 | ``` 57 | fvid -i [input video] -d 58 | ``` 59 | 60 | Windows 61 | 62 | ``` 63 | py -m fvid -i [input video] -d 64 | ``` 65 | 66 | If the file was encoded with a non-default password, it'll prompt you to enter the password upon decoding. 67 | 68 | How to Contribute 69 | ----------------- 70 | 71 | 1. Check for open issues or open a fresh issue to start a discussion 72 | around a feature idea or a bug [here](https://github.com/AlfredoSequeida/fvid/issues) 73 | tag for issues that should be ideal for people who are not very familiar with the codebase yet. 74 | 2. Fork [the repository](https://github.com/alfredosequeida/fvid) on 75 | GitHub to start making your changes to the **master** branch (or branch off of it). 76 | 3. Write a test which shows that the bug was fixed or that the feature 77 | works as expected. 78 | 4. Send a pull request and bug the maintainer until it gets merged and 79 | published. :) 80 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx 2 | recommonmark -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -- General configuration ----------------------------------------------------- 2 | import os 3 | import sys 4 | 5 | # Use the source tree. 6 | sys.path.insert(1, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 7 | 8 | # Add any Sphinx extension module names here, as strings. They can be extensions 9 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 10 | extensions = [ 11 | 'sphinx.ext.autodoc', 12 | 'sphinx.ext.doctest', 13 | 'sphinx.ext.intersphinx', 14 | 'sphinx.ext.todo', 15 | 'sphinx.ext.coverage', 16 | 'recommonmark' 17 | ] 18 | 19 | # Add any paths that contain templates here, relative to this directory. 20 | templates_path = ['_templates'] 21 | 22 | # The suffix of source filenames. 23 | source_suffix = '.rst' 24 | 25 | # The master toctree document. 26 | master_doc = 'index' 27 | 28 | # General information about the project. 29 | copyright = '2020-2021 Alfredo Sequeida' 30 | 31 | # List of directories, relative to source directory, that shouldn't be searched 32 | # for source files. 33 | exclude_trees = [] 34 | 35 | # List of patterns, relative to source directory, that match files and 36 | # directories to ignore when looking for source files. 37 | # This pattern also affects html_static_path and html_extra_path. 38 | exclude_patterns = [] 39 | 40 | # The name of the Pygments (syntax highlighting) style to use. 41 | pygments_style = 'sphinx' 42 | 43 | # -- Options for HTML output --------------------------------------------------- 44 | 45 | # The theme to use for HTML and HTML Help pages. Major themes that come with 46 | # Sphinx are currently 'default' and 'sphinxdoc'. 47 | html_theme = 'default' 48 | 49 | # Output file base name for HTML help builder. 50 | htmlhelp_basename = 'fviddoc' 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 | 57 | # -- Options for LaTeX output -------------------------------------------------- 58 | 59 | # Grouping the document tree into LaTeX files. List of tuples 60 | # (source start file, target name, title, author, documentclass [howto/manual]). 61 | latex_documents = [ 62 | ('index', 'fvid.tex', 'Fvid Documentation', 'Alfredo Sequeida', 'manual'), 63 | ] 64 | 65 | 66 | # Example configuration for intersphinx: refer to the Python standard library. 67 | intersphinx_mapping = {'https://docs.python.org/3': None} -------------------------------------------------------------------------------- /docs/source/examples.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ******** 3 | 4 | **If you are on Windows, prefix all commands with** `py -m` 5 | 6 | Encoding 7 | ======== 8 | All of these examples assume you have a file named test.jpg inside your current directory. 9 | 10 | Basic: ``fvid -i ./test.jpg -e`` 11 | 12 | 3 FPS: ``fvid -i ./test.jpg -e -f 3`` 13 | 14 | 3 FPS + Password: ``fvid -i ./test.jpg -e -f 3 -p "testing testing 123"`` 15 | 16 | Advanced: ``fvid -i ./test.jpg -ez5 -f 3 -p "ez5 is short for encode, zfec, h265"`` 17 | 18 | Decoding 19 | ======== 20 | All of these examples assume you have an fvid-encoded video named file.mp4 inside your current directory. 21 | 22 | Basic: ``fvid -i ./file.mp4 -d`` 23 | 24 | Encoded at 3 FPS: ``fvid -i ./file.mp4 -d -f 3`` 25 | 26 | Encoded at 3 FPS + Password: ``fvid -i ./file.mp4 -d -f 3`` (It'll prompt you to enter the password) 27 | 28 | Advanced: ``fvid -i ./file.mp4 -dz5 -f 3`` -------------------------------------------------------------------------------- /docs/source/history.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ********* 3 | 4 | Planned (1.1.0) 5 | =============== 6 | 7 | - GUI 8 | - Use json-encoded strings instead of pickle for storing stuff 9 | - Add `-5` flag for H265 encoding/decoding, much smaller output file 10 | - Add zfec error correction 11 | 12 | 1.0.0 13 | ===== 14 | 15 | Additions: 16 | 17 | - Added support for passwords, custom framerates, and Cython compilation 18 | - file.mp4 compression with ``gzip`` 19 | - Pickled data to allow decompression with original file name 20 | 21 | Bug Fixes: 22 | 23 | - Fixed ``file.bin`` bug 24 | - Removed ``magic``, ``mime`` 25 | - New ``make_image_sequence`` logic 26 | - Framerate patch 27 | 28 | Performance: 29 | 30 | - Huge Cython/Python speedups 31 | 32 | 0.0.2 33 | ===== 34 | 35 | - Bug fixes 36 | - Minor speedups 37 | 38 | 0.0.1 39 | ===== 40 | 41 | - Initial release 42 | -------------------------------------------------------------------------------- /docs/source/howto.rst: -------------------------------------------------------------------------------- 1 | How To 2 | ****** 3 | 4 | Basic Usage 5 | =========== 6 | 7 | Assuming you have pip installed fvid, you can use encode videos as follows: 8 | 9 | Linux/OSX: 10 | 11 | ``fvid -i [input file] -e`` 12 | 13 | ``fvid -i [input file] --framerate 1 -e`` 14 | 15 | ``fvid -i [input file] --password "wow fvid is cool" -e`` 16 | 17 | Windows: 18 | 19 | ``py -m fvid -i [input file] -e`` 20 | 21 | ``py -m fvid -i [input file] --framerate 1 -e`` 22 | 23 | ``py -m fvid -i [input file] --password "wow fvid is cool" -e`` 24 | 25 | 26 | Decoding is supported also: 27 | 28 | Linux/OSX: 29 | 30 | ``fvid -i [input video] -d`` 31 | 32 | Windows: 33 | 34 | ``py -m fvid -i [input video] -d`` 35 | 36 | 37 | If the file was encoded with a non-default password, it'll prompt you to enter the password upon decoding. 38 | 39 | Options 40 | ======= 41 | 42 | * ``-i/--input`` After this, specify the path to the file you want to encode/decode. 43 | * ``-o/--output`` After this, specify the name of the new file you want to make. Defaults to ``file.mp4`` when encoding. 44 | * ``-e/--encode`` This indicates you're encoding the input file. 45 | * ``-d/--decode`` This indicates you're decoding the input file. 46 | * ``-y/--overwrite`` This indicates that you want ffmpeg to automatically overwrite a previously encoded file if it needs to. 47 | * ``-f/--framerate`` After this, specify the framerate that you want to have the file output at. A higher value means the frames go faster. Defaults to 1 fps. 48 | * ``-p/--password`` After this, specify the password you want to encode with. You will need to remember this to decode the file. Defaults to 32 spaces. -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Fvid documentation master file, created by 2 | sphinx-quickstart on Sat Feb 20 11:43:24 2021. 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 Fvid's documentation! 7 | ================================ 8 | 9 | .. toctree:: 10 | :maxdepth: 4 11 | :caption: Contents: 12 | 13 | history 14 | howto 15 | examples 16 | errors 17 | 18 | Download & Install 19 | ================== 20 | 21 | The easiest way to get fvid is via PyPi_ with pip_:: 22 | 23 | $ pip install -U fvid 24 | 25 | You can also ``git clone`` the latest code and install from source:: 26 | 27 | $ python setup.py install 28 | 29 | .. _PyPi: https://pypi.org/project/fvid/ 30 | .. _pip: https://pypi.org/project/pip/ 31 | .. _clone: https://github.com/AlfredoSequeida/fvid.git 32 | 33 | Indices and tables 34 | ================== 35 | 36 | * :ref:`genindex` 37 | * :ref:`modindex` 38 | * :ref:`search` 39 | 40 | 41 | Authors 42 | ======= 43 | 44 | * Alfredo Sequeida 45 | * Wisketchy Dobrov 46 | * Theelgirl 47 | 48 | Pages 49 | ===== 50 | * :doc:`/index` 51 | * :doc:`/history` 52 | * :doc:`/howto` 53 | * :doc:`/examples` 54 | * :doc:`/errors` -------------------------------------------------------------------------------- /fvid/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.0.0" 2 | 3 | -------------------------------------------------------------------------------- /fvid/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | # leaving this here in case the try/except thing doesn't work 4 | """ 5 | import platform 6 | import distro # to check linux distributions 7 | 8 | try: 9 | linux_distro = distro.linux_distribution()[0].lower() 10 | except: 11 | linux_distro = "n/a" 12 | 13 | # fvid 14 | if platform.system().lower() in ('linux', 'darwin') and linux_distro not in ('artix linux',): 15 | # this used to work for every distro but something changed in the Cython/Password PR 16 | from fvid import main 17 | else: 18 | # windows and artix linux need this because of something in the Cython/Password PR, unknown if more OSes need it 19 | from fvid.fvid import main 20 | """ 21 | 22 | try: 23 | from fvid import main 24 | except: 25 | from fvid.fvid import main 26 | 27 | if __name__ == '__main__': 28 | sys.exit(main()) 29 | -------------------------------------------------------------------------------- /fvid/cythonizer.py: -------------------------------------------------------------------------------- 1 | #cython: language_level=3 2 | # distutils: include_dirs = /root/fvid, /rood/fvid/tests 3 | from distutils.core import Extension, setup 4 | from Cython.Build import cythonize 5 | 6 | ext = Extension(name="fvid_cython", sources=["fvid_cython.pyx"], include_dirs=['/root/fvid', '/root/fvid/tests']) 7 | setup(ext_modules=cythonize(ext, annotate=True, compiler_directives={'language_level': 3, 'infer_types': True})) 8 | -------------------------------------------------------------------------------- /fvid/fvid.py: -------------------------------------------------------------------------------- 1 | from bitstring import Bits, BitArray 2 | from PIL import Image 3 | import glob 4 | from tqdm import tqdm 5 | import binascii 6 | import argparse 7 | import sys 8 | import os 9 | import getpass 10 | import io 11 | import gzip 12 | import json 13 | import base64 14 | import decimal 15 | import random 16 | import magic 17 | 18 | from zfec import easyfec as ef 19 | 20 | from cryptography.hazmat.backends import default_backend 21 | from cryptography.hazmat.primitives import hashes 22 | from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC 23 | from Crypto.Cipher import AES 24 | 25 | try: 26 | from fvid_cython import cy_gbfi, cy_gbfi_h265, cy_encode_zfec 27 | 28 | use_cython = True 29 | except (ImportError, ModuleNotFoundError): 30 | use_cython = False 31 | 32 | FRAMES_DIR = "./fvid_frames/" 33 | SALT = ( 34 | "63929291bca3c602de64352a4d4bfe69".encode() 35 | ) # It needs be the same in one instance of coding/decoding 36 | DEFAULT_KEY = " " * 32 37 | DEFAULT_KEY = DEFAULT_KEY.encode() 38 | NOTDEBUG = True 39 | TEMPVIDEO = "_temp.mp4" 40 | FRAMERATE = "1" 41 | 42 | # DO NOT CHANGE: (2, 3-8) works sometimes 43 | # this is the most efficient by far though 44 | KVAL = 4 45 | MVAL = 5 46 | # this can by ANY integer that is a multiple of (KVAL/MVAL) 47 | # but it MUST stay the same between encoding/decoding 48 | # reccomended 8-64 49 | BLOCK = 16 50 | 51 | 52 | class WrongPassword(Exception): 53 | pass 54 | 55 | 56 | class MissingArgument(Exception): 57 | pass 58 | 59 | 60 | def get_password(password_provided: str) -> bytes: 61 | """ 62 | Prompt user for password and create a key for decrypting/encrypting 63 | 64 | password_provided: password provided by tge user with -p flag 65 | """ 66 | 67 | if password_provided == "default": 68 | return DEFAULT_KEY 69 | else: 70 | if password_provided == None: 71 | password_provided = getpass.getpass("Enter password:") 72 | 73 | password = str(password_provided).encode() 74 | kdf = PBKDF2HMAC( 75 | algorithm=hashes.SHA512(), 76 | length=32, 77 | salt=SALT, 78 | iterations=100000, 79 | backend=default_backend(), 80 | ) 81 | key = kdf.derive(password) 82 | return key 83 | 84 | def encode_zfec(bit_array: BitArray) -> BitArray: 85 | global KVAL, MVAL, BLOCK 86 | """ 87 | Apply Reed-Solomon error correction every byte to maximize retrieval 88 | possibility as opposed to applying it to the entire file 89 | 90 | bit_array -- BitArray containing raw file data 91 | ecc -- The error correction value to be used (default DEFAULT_ECC) 92 | """ 93 | 94 | bits = bit_array.bin 95 | 96 | if use_cython: 97 | return BitArray(bytes=cy_encode_zfec(bits).encode('utf-8')) 98 | else: 99 | # split bits into blocks of bits 100 | byte_list = split_string_by_n(bits, BLOCK) 101 | 102 | ecc_bytes = "" 103 | 104 | print("Applying Zfec Error Correction...") 105 | 106 | encoder = ef.Encoder(KVAL, MVAL) 107 | for b in tqdm(byte_list): 108 | ecc_bytes += ''.join(map(bytes.decode, encoder.encode(b.encode('utf-8')))) 109 | 110 | return BitArray(bytes=ecc_bytes.encode('utf-8')) 111 | 112 | def get_bits_from_file( 113 | filepath: str, key: bytes, zfec: bool 114 | ) -> BitArray: 115 | """ 116 | Get/read bits fom file, encrypt data, and zip 117 | 118 | filepath -- the file to read 119 | key -- key used to encrypt file 120 | zfec -- if reed solomon should be used to encode bits 121 | """ 122 | 123 | print("Reading file...") 124 | 125 | bitarray = BitArray(filename=filepath) 126 | 127 | if zfec: 128 | bitarray = encode_zfec(bitarray) 129 | 130 | # encrypt data 131 | cipher = AES.new(key, AES.MODE_EAX, nonce=SALT) 132 | ciphertext, tag = cipher.encrypt_and_digest(bitarray.tobytes()) 133 | 134 | filename = os.path.basename(filepath) 135 | 136 | # because json can only serialize strings, the byte objects are encoded 137 | # using base64 138 | 139 | data_bytes = json.dumps( 140 | { 141 | "tag": base64.b64encode(tag).decode("utf-8"), 142 | "data": base64.b64encode(ciphertext).decode("utf-8"), 143 | "filename": filepath, 144 | } 145 | ).encode("utf-8") 146 | 147 | # print("Zipping...") 148 | 149 | # zip 150 | out = io.BytesIO() 151 | with gzip.GzipFile(fileobj=out, mode="w") as fo: 152 | fo.write(data_bytes) 153 | zip = out.getvalue() 154 | # zip 155 | 156 | del bitarray 157 | 158 | bitarray = BitArray(zip) 159 | # bitarray = BitArray(data_bytes) 160 | 161 | return bitarray.bin 162 | 163 | 164 | def get_bits_from_image(image: Image, use_h265: bool) -> str: 165 | """ 166 | extract bits from image (frame) pixels 167 | 168 | image -- png image file used to extract bits from 169 | """ 170 | 171 | # use two different functions so we can type pixel correctly 172 | if use_cython and not use_h265: 173 | return cy_gbfi(image) 174 | elif use_cython and use_h265: 175 | return cy_gbfi_h265(image) 176 | 177 | width, height = image.size 178 | 179 | px = image.load() 180 | bits = "" 181 | 182 | # use separate code path so we dont check inside every loop 183 | if not use_h265: 184 | for y in range(height): 185 | for x in range(width): 186 | pixel = px[x, y] 187 | pixel_bin_rep = "0" 188 | 189 | # if the white difference is smaller, that means the pixel is 190 | # closer to white, otherwise, the pixel must be black 191 | if ( 192 | abs(pixel[0] - 255) < abs(pixel[0] - 0) 193 | and abs(pixel[1] - 255) < abs(pixel[1] - 0) 194 | and abs(pixel[2] - 255) < abs(pixel[2] - 0) 195 | ): 196 | pixel_bin_rep = "1" 197 | 198 | # adding bits 199 | bits += pixel_bin_rep 200 | else: 201 | for y in range(height): 202 | for x in range(width): 203 | # 1 if it's a white pixel, otherwise it's black so 0 204 | bits += "1" if px[x, y] == 255 else "0" 205 | 206 | return bits 207 | 208 | 209 | def get_bits_from_video(video_filepath: str, use_h265: bool, overwrite: bool = False) -> str: 210 | """ 211 | extract the bits from a video by frame (using a sequence of images) 212 | 213 | video_filepath -- The file path for the video to extract bits from 214 | """ 215 | 216 | print("Reading video...") 217 | 218 | image_sequence = [] 219 | if use_h265: 220 | cmd = ( 221 | "ffmpeg -i '" 222 | + video_filepath 223 | + "' -c:v libx265 -filter:v fps=fps=" 224 | + FRAMERATE 225 | + " -x265-params lossless=1 -tune grain ") 226 | if overwrite: 227 | cmd += "-y " 228 | if NOTDEBUG: 229 | cmd += "-loglevel fatal " + TEMPVIDEO 230 | else: 231 | cmd += TEMPVIDEO 232 | os.system(cmd) 233 | else: 234 | cmd = ( 235 | "ffmpeg -i '" 236 | + video_filepath 237 | + "' -c:v libx264rgb -filter:v fps=fps=" 238 | + FRAMERATE 239 | + " ") 240 | if overwrite: 241 | cmd += "-y " 242 | if NOTDEBUG: 243 | cmd += "-loglevel fatal " + TEMPVIDEO 244 | else: 245 | cmd += TEMPVIDEO 246 | os.system(cmd) 247 | 248 | cmd = "ffmpeg -i " + TEMPVIDEO + " ./fvid_frames/decoded_frames_%d.png" 249 | if NOTDEBUG: 250 | cmd += " -loglevel fatal" 251 | os.system(cmd) 252 | os.remove(TEMPVIDEO) 253 | 254 | for filename in sorted( 255 | glob.glob(f"{FRAMES_DIR}decoded_frames*.png"), key=os.path.getmtime 256 | ): 257 | image_sequence.append(Image.open(filename)) 258 | 259 | bits = "" 260 | sequence_length = len(image_sequence) 261 | print("Bits are in place") 262 | 263 | if use_cython: 264 | print("Using Cython...") 265 | 266 | for index in tqdm(range(sequence_length)): 267 | bits += get_bits_from_image(image_sequence[index], use_h265) 268 | 269 | return bits 270 | 271 | 272 | def decode_zfec(data_bytes: bytes) -> bytes: 273 | global KVAL, MVAL, BLOCK 274 | 275 | byte_list = split_string_by_n(data_bytes, int(BLOCK*(MVAL/KVAL))) 276 | 277 | # appending to a single bytes object is very slow so we make 50-51 and combine at the end 278 | decoded_bytes = [bytes()] * (int(len(byte_list) / 50) + 1) 279 | 280 | print("Decoding Zfec Error Correction...") 281 | 282 | decoder = ef.Decoder(KVAL, MVAL) 283 | i = 0 284 | for b in tqdm(byte_list): 285 | base = split_string_by_n(b, len(b) // MVAL) 286 | decoded_str_1 = decoder.decode(base[:KVAL], list(range(KVAL)), 0) 287 | decoded_str_2 = decoder.decode(base[1:KVAL+1], list(range(KVAL+1))[1:], 0) 288 | if decoded_str_1 == decoded_str_2: 289 | decoded_bytes[i//50] += decoded_str_1 290 | else: # its corrupted here 291 | j = 10 292 | while j > 0 and decoded_str_1 != decoded_str_2: 293 | random.shuffle(base) 294 | decoded_str_1 = decoder.decode(base[:KVAL], list(range(KVAL)), 0) 295 | decoded_str_2 = decoder.decode(base[1:KVAL+1], list(range(KVAL+1))[1:], 0) 296 | j -= 1 297 | decoded_bytes[i//50] += decoded_str_1 # it should be correct by now 298 | i += 1 299 | 300 | decoded_bytestring = bytes() 301 | 302 | for bytestring in tqdm(decoded_bytes): 303 | decoded_bytestring += bytestring 304 | 305 | return decoded_bytestring 306 | 307 | 308 | def save_bits_to_file( 309 | file_path: str, bits: str, key: bytes, zfec: bool 310 | ): 311 | """ 312 | save/write bits to a file 313 | 314 | file_path -- the path to write to 315 | bits -- the bits to write 316 | key -- key userd for file decryption 317 | zfec -- needed if reed solomon was used to encode bits 318 | """ 319 | 320 | bitstring = Bits(bin=bits) 321 | 322 | # zip 323 | print("Unziping...") 324 | in_ = io.BytesIO() 325 | in_.write(bitstring.bytes) 326 | in_.seek(0) 327 | # always fails without this but sometimes work with this, unsure why 328 | filetype = magic.from_buffer(in_.read()) 329 | print(filetype) 330 | in_.seek(0) 331 | with gzip.GzipFile(fileobj=in_, mode="rb") as fo: 332 | bitstring = fo.read() 333 | # zip 334 | 335 | # loading data back from bytes to utf-8 string to deserialize 336 | data = json.loads(bitstring.decode("utf-8")) 337 | 338 | # decoding previously encoded base64 bytes data to get bytes back 339 | tag = base64.b64decode(data["tag"]) 340 | ciphertext = base64.b64decode(data["data"]) 341 | 342 | filename = data["filename"] 343 | 344 | # decrypting data 345 | cipher = AES.new(key, AES.MODE_EAX, nonce=SALT) 346 | data_bytes = cipher.decrypt(ciphertext) 347 | 348 | print("Checking integrity...") 349 | 350 | try: 351 | cipher.verify(tag) 352 | except ValueError: 353 | raise WrongPassword("Key incorrect or message corrupted") 354 | 355 | bitstring = Bits(data_bytes) 356 | 357 | if zfec: 358 | bitstring = Bits( 359 | "0b" + decode_zfec(data_bytes).decode("utf-8") 360 | ) 361 | 362 | # If filepath not passed in use default otherwise used passed in filepath 363 | if file_path == None: 364 | filepath = filename 365 | else: 366 | filepath = file_path 367 | 368 | with open(filepath, "wb") as f: 369 | bitstring.tofile(f) 370 | 371 | 372 | def split_string_by_n(bitstring: str, n: int) -> list: 373 | """ 374 | Split a string every n number of characters 375 | (or less if the 'remaining characters' < n ) this way we can sperate the 376 | data for an etire video into a list based on the resolution of a frame. 377 | 378 | bitstring -- a string containing bits 379 | n -- split the string every n characters, for example to split a 380 | 1920 x 1080 frame, this would be 1920*1080 = 2073600 381 | """ 382 | 383 | bit_list = [] 384 | 385 | for i in range(0, len(bitstring), n): 386 | bit_list.append(bitstring[i : i + n]) 387 | 388 | return bit_list 389 | 390 | 391 | def make_image_sequence(bitstring: BitArray, resolution: tuple = (1920, 1080)): 392 | """ 393 | Create image sequence (frames) for a video 394 | 395 | bitstring -- BitArray of bits used to create pixels with bit data 396 | resolution -- the resoultion used for each frame (default 1920x1080) 397 | """ 398 | 399 | width, height = resolution 400 | 401 | # split bits into sets of width*height to make (1) image 402 | set_size = width * height 403 | 404 | # bit_sequence = [] 405 | print("Making image sequence") 406 | print("Cutting...") 407 | 408 | bitlist = split_string_by_n(bitstring, set_size) 409 | 410 | del bitstring 411 | 412 | bitlist[-1] = bitlist[-1] + "0" * (set_size - len(bitlist[-1])) 413 | 414 | index = 1 415 | bitlist = bitlist[::-1] 416 | 417 | print("Saving frames...") 418 | 419 | for _ in tqdm(range(len(bitlist))): 420 | bitl = bitlist.pop() 421 | image_bits = list(map(int, bitl)) 422 | 423 | image = Image.new("1", (width, height)) 424 | image.putdata(image_bits) 425 | image.save(f"{FRAMES_DIR}encoded_frames_{index}.png") 426 | index += 1 427 | 428 | 429 | def make_video(output_filepath: str, framerate: int = FRAMERATE, use_h265: bool = False, overwrite: bool = False): 430 | """ 431 | Create video using ffmpeg 432 | 433 | output_filepath -- the output file path where to store the video 434 | framerate -- the framerate for the vidoe (default 1) 435 | """ 436 | 437 | if output_filepath == None: 438 | outputfile = "file.mp4" 439 | else: 440 | outputfile = output_filepath 441 | 442 | if use_h265: 443 | cmd = ( 444 | "ffmpeg -r " 445 | + framerate 446 | + " -i ./fvid_frames/encoded_frames_%d.png -c:v libx265 " 447 | + " -x265-params lossless=1 -tune grain ") 448 | if overwrite: 449 | cmd += "-y " 450 | if NOTDEBUG: 451 | cmd += "-loglevel fatal " + outputfile 452 | else: 453 | cmd += outputfile 454 | os.system(cmd) 455 | else: 456 | cmd = ( 457 | "ffmpeg -r " 458 | + framerate 459 | + " -i ./fvid_frames/encoded_frames_%d.png -c:v libx264rgb ") 460 | if overwrite: 461 | cmd += "-y " 462 | if NOTDEBUG: 463 | cmd += "-loglevel fatal " + outputfile 464 | else: 465 | cmd += outputfile 466 | os.system(cmd) 467 | 468 | def cleanup(): 469 | """ 470 | Clean up the files (frames) creted by fvid during encoding/decoding 471 | """ 472 | import shutil 473 | 474 | shutil.rmtree(FRAMES_DIR) 475 | 476 | 477 | def setup(): 478 | """ 479 | setup fvid directory used to store frames for encoding/decoding 480 | """ 481 | 482 | if not os.path.exists(FRAMES_DIR): 483 | os.makedirs(FRAMES_DIR) 484 | 485 | def main(): 486 | global FRAMERATE 487 | parser = argparse.ArgumentParser(description="save files as videos") 488 | parser.add_argument( 489 | "-e", "--encode", help="encode file as video", action="store_true" 490 | ) 491 | parser.add_argument( 492 | "-d", "--decode", help="decode file from video", action="store_true" 493 | ) 494 | 495 | parser.add_argument("-i", "--input", help="input file", required=True) 496 | parser.add_argument("-o", "--output", help="output path") 497 | parser.add_argument( 498 | "-f", 499 | "--framerate", 500 | help="set framerate for encoding (as a fraction)", 501 | default=FRAMERATE, 502 | type=str, 503 | ) 504 | parser.add_argument( 505 | "-p", 506 | "--password", 507 | help="set password", 508 | nargs="?", 509 | type=str, 510 | default="default", 511 | ) 512 | parser.add_argument( 513 | "-z", 514 | "--zfec", 515 | help=( 516 | "Apply Zfec error correcting. This is helpful if you're" 517 | " finding that your data is not being decoded correctly. It adds" 518 | " 2 extra bits per byte making it possible to recover all 8 bits" 519 | " in the case the data changes during the decoding process at" 520 | " the cost of making your video files larger. Note, if you use" 521 | " this option, you must also use the -r flag to decode a video" 522 | " back to a file, otherwise, your data will not be recovered" 523 | " correctly." 524 | ), 525 | action="store_true", 526 | ) 527 | parser.add_argument( 528 | "-5", 529 | "--h265", 530 | help="Use H.265 codec for improved efficiency", 531 | action="store_true", 532 | ) 533 | parser.add_argument( 534 | "-y", 535 | "--overwrite", 536 | help="Automatically overwrite file if it exists (FFMPEG)", 537 | action="store_true", 538 | ) 539 | 540 | args = parser.parse_args() 541 | 542 | setup() 543 | 544 | if not NOTDEBUG: 545 | print("args", args) 546 | print( 547 | "PASSWORD", 548 | args.password, 549 | [ 550 | len(args.password) if len(args.password) is not None else None 551 | for _ in range(0) 552 | ], 553 | ) 554 | 555 | # using default framerate if none is provided by the user 556 | if args.framerate != FRAMERATE: 557 | FRAMERATE = args.framerate 558 | 559 | # check for arguments 560 | if not args.decode and not args.encode: 561 | raise MissingArgument("You should use either --encode or --decode!") 562 | 563 | key = get_password(args.password) 564 | 565 | if args.decode: 566 | bits = get_bits_from_video(args.input, args.h265, args.overwrite) 567 | 568 | file_path = None 569 | 570 | if args.output: 571 | file_path = args.output 572 | 573 | save_bits_to_file(file_path, bits, key, args.zfec) 574 | 575 | elif args.encode: 576 | 577 | # isdigit has the benefit of being True and raising an error if the 578 | # user passes a negative string 579 | # all() lets us check if both the negative sign and forward slash are 580 | # in the string, to prevent negative fractions 581 | if (not args.framerate.isdigit() and "/" not in args.framerate) or all( 582 | x in args.framerate for x in ("-", "/") 583 | ): 584 | raise NotImplementedError( 585 | "The framerate must be a positive fraction or an integer for " 586 | "now, like 3, '1/3', or '1/5'!" 587 | ) 588 | 589 | # get bits from file 590 | bits = get_bits_from_file(args.input, key, args.zfec) 591 | 592 | # create image sequence 593 | make_image_sequence(bits) 594 | 595 | video_file_path = None 596 | 597 | if args.output: 598 | video_file_path = args.output 599 | 600 | make_video(video_file_path, args.framerate, args.h265, args.overwrite) 601 | 602 | cleanup() 603 | 604 | if __name__ == '__main__': 605 | main() 606 | -------------------------------------------------------------------------------- /fvid/fvid_cython.pyx: -------------------------------------------------------------------------------- 1 | # distutils: language=c++ 2 | # cython: boundscheck=False 3 | # cython: cdivision=True 4 | # cython: wraparound=False 5 | # cython: nonecheck=False 6 | # cython: c_string_type=unicode, c_string_encoding=ascii 7 | from tqdm import tqdm 8 | from zfec import easyfec as ef 9 | from libcpp.string cimport string 10 | 11 | cpdef str cy_gbfi(image): 12 | cdef int width, height, x, y 13 | cdef string bits = b'' 14 | cdef (int, int, int) pixel 15 | 16 | width, height = image.size 17 | 18 | px = image.load() 19 | 20 | for y in range(height): 21 | for x in range(width): 22 | pixel = px[x, y] 23 | 24 | # if the white difference is smaller (comparison part 1), that means the pixel is closer 25 | # to white, otherwise, the pixel must be black 26 | bits.append(b'1' if abs(pixel[0] - 255) < abs(pixel[0] - 0) and abs(pixel[1] - 255) < abs(pixel[1] - 0) and abs(pixel[2] - 255) < abs(pixel[2] - 0) else b'0') 27 | 28 | return bits 29 | 30 | cpdef str cy_gbfi_h265(image): 31 | cdef int width, height, x, y 32 | cdef string bits = b'' 33 | 34 | width, height = image.size 35 | 36 | px = image.load() 37 | 38 | for y in range(height): 39 | for x in range(width): 40 | bits.append(b'1' if px[x, y] == 255 else b'0') 41 | 42 | return bits 43 | 44 | cpdef str cy_encode_zfec(string bits): 45 | cdef int b, KVAL = 4, MVAL = 5, BLOCK = 16 46 | cdef string ecc_bytes = b'', byte 47 | cdef tuple byte_tuple 48 | 49 | # cdef (string, string, string, string, string) temp 50 | 51 | byte_tuple = split_string_by_n(bits, BLOCK) 52 | 53 | print("Applying Zfec Error Correction...") 54 | 55 | encoder = ef.Encoder(KVAL, MVAL) 56 | 57 | for b in tqdm(range(len(byte_tuple))): 58 | for byte in encoder.encode(byte_tuple[b].encode('utf-8')): 59 | ecc_bytes.append(byte) 60 | 61 | return ecc_bytes 62 | 63 | cdef tuple split_string_by_n(str bitstring, int n): 64 | cdef list bit_list = [] 65 | 66 | for i in range(0, len(bitstring), n): 67 | bit_list.append(bitstring[i:i+n]) 68 | 69 | return tuple(bit_list) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow>=7.0.0 2 | tqdm>=4.25.0 3 | bitstring>=3.1.6 4 | pycryptography>=3.1.1 5 | zfec -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import codecs 3 | from setuptools import setup 4 | from setuptools import Extension 5 | from setuptools.command.build_ext import build_ext 6 | 7 | try: 8 | from Cython.Build import cythonize 9 | except ImportError: 10 | use_cython = False 11 | ext = 'c' 12 | else: 13 | use_cython = True 14 | ext = 'pyx' 15 | 16 | try: 17 | if not use_cython: 18 | extensions = [Extension("fvid.fvid_cython", ["fvid/fvid_cython.c"], include_dirs=["./fvid", "fvid/"])] 19 | else: 20 | extensions = [Extension("fvid.fvid_cython", ["fvid/fvid_cython.pyx"], include_dirs=["./fvid", "fvid/"])] 21 | extensions = cythonize(extensions, compiler_directives={'language_level': "3", 'infer_types': True}) 22 | except: # blanket exception until the exact exception name is found 23 | extensions = [] 24 | 25 | with open("README.md", "r") as fh: 26 | long_description = fh.read() 27 | 28 | 29 | def read(rel_path): 30 | here = os.path.abspath(os.path.dirname(__file__)) 31 | with codecs.open(os.path.join(here, rel_path), "r") as fp: 32 | return fp.read() 33 | 34 | 35 | def get_version(rel_path): 36 | for line in read(rel_path).splitlines(): 37 | if line.startswith("__version__"): 38 | delim = '"' if '"' in line else "'" 39 | return line.split(delim)[1] 40 | else: 41 | raise RuntimeError("Unable to find version string.") 42 | 43 | 44 | dynamic_version = get_version("fvid/__init__.py") 45 | 46 | try: 47 | setup( 48 | name="fvid", 49 | version=dynamic_version, 50 | author="Alfredo Sequeida", 51 | description="fvid is a project that aims to encode any file as a video using 1-bit color images to survive compression algorithms for data retrieval.", 52 | long_description=long_description, 53 | long_description_content_type="text/markdown", 54 | url="https://github.com/AlfredoSequeida/fvid", 55 | download_url="https://github.com/AlfredoSequeida/fvid/archive/" 56 | + dynamic_version 57 | + ".tar.gz", 58 | keywords="fvid youtube videos files bitdum hexdump ffmpeg video file", 59 | platforms="any", 60 | classifiers=[ 61 | "Intended Audience :: End Users/Desktop", 62 | "License :: OSI Approved :: MIT License", 63 | "Programming Language :: Python :: 3", 64 | "Programming Language :: Python :: 3.6", 65 | "Programming Language :: Python :: 3.7", 66 | "Programming Language :: Python :: 3.8", 67 | "Programming Language :: Python :: 3.9", 68 | "Operating System :: Microsoft :: Windows :: Windows 10", 69 | "Operating System :: Microsoft :: Windows :: Windows 8", 70 | "Operating System :: Microsoft :: Windows :: Windows 8.1", 71 | "Operating System :: MacOS :: MacOS X", 72 | "Operating System :: POSIX :: Linux", 73 | ], 74 | license="MIT", 75 | packages=["fvid"], 76 | install_requires=[ 77 | "bitstring >= 3.1.6", 78 | "pillow >= 7.2.0", 79 | "tqdm >= 4.49.0", 80 | "cryptography >= 3.1.1", 81 | "pycryptodome >= 3.9.8" 82 | ], 83 | python_requires=">=3.6", 84 | entry_points={"console_scripts": ["fvid = fvid.fvid:main"]}, 85 | cmdclass={'build_ext': build_ext}, 86 | ext_modules=extensions, 87 | include_package_data=True, 88 | zip_safe=False, 89 | ) 90 | except: 91 | setup( 92 | name="fvid", 93 | version=dynamic_version, 94 | author="Alfredo Sequeida", 95 | description="fvid is a project that aims to encode any file as a video using 1-bit color images to survive compression algorithms for data retrieval.", 96 | long_description=long_description, 97 | long_description_content_type="text/markdown", 98 | url="https://github.com/AlfredoSequeida/fvid", 99 | download_url="https://github.com/AlfredoSequeida/fvid/archive/" 100 | + dynamic_version 101 | + ".tar.gz", 102 | keywords="fvid youtube videos files bitdum hexdump ffmpeg video file", 103 | platforms="any", 104 | classifiers=[ 105 | "Intended Audience :: End Users/Desktop", 106 | "License :: OSI Approved :: MIT License", 107 | "Programming Language :: Python :: 3", 108 | "Programming Language :: Python :: 3.6", 109 | "Programming Language :: Python :: 3.7", 110 | "Programming Language :: Python :: 3.8", 111 | "Programming Language :: Python :: 3.9", 112 | "Operating System :: Microsoft :: Windows :: Windows 10", 113 | "Operating System :: Microsoft :: Windows :: Windows 8", 114 | "Operating System :: Microsoft :: Windows :: Windows 8.1", 115 | "Operating System :: MacOS :: MacOS X", 116 | "Operating System :: POSIX :: Linux", 117 | ], 118 | license="MIT", 119 | packages=["fvid"], 120 | install_requires=[ 121 | "bitstring >= 3.1.6", 122 | "pillow >= 7.2.0", 123 | "tqdm >= 4.49.0", 124 | "cryptography >= 3.1.1", 125 | "pycryptodome >= 3.9.8" 126 | ], 127 | python_requires=">=3.6", 128 | entry_points={"console_scripts": ["fvid = fvid.fvid:main"]}, 129 | include_package_data=True, 130 | ) 131 | -------------------------------------------------------------------------------- /tests/decoded/README.txt: -------------------------------------------------------------------------------- 1 | large_pdf.pdf: 2 | - yt fvid -d.mp4 3 | - yt fvid -dz.mp4 4 | 5 | rio_de_janeiro_skyline.jpg: 6 | - yt fvid -dz5.mp4 7 | - fvid -dz5.mp4 -------------------------------------------------------------------------------- /tests/decoded/large_pdf.pdf.REMOVED.git-id: -------------------------------------------------------------------------------- 1 | a28df2863dd91173b0baa5043b8a15ff6ae67b85 -------------------------------------------------------------------------------- /tests/decoded/rio_de_janeiro_skyline.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlfredoSequeida/fvid/7f1bdcf8d78b899c0224f89e669b0d85aed6df56/tests/decoded/rio_de_janeiro_skyline.jpg -------------------------------------------------------------------------------- /tests/encoded/fvid -dyz5.mp4.REMOVED.git-id: -------------------------------------------------------------------------------- 1 | 11240b442e7a29da7eef80457e0d726f690ed794 -------------------------------------------------------------------------------- /tests/encoded/yt fvid -dy.mp4.REMOVED.git-id: -------------------------------------------------------------------------------- 1 | caa3defe33182e8d166539f2c8e93d80779fb485 -------------------------------------------------------------------------------- /tests/encoded/yt fvid -dyz5.mp4.REMOVED.git-id: -------------------------------------------------------------------------------- 1 | 9b0e4fbde08c8b11e7a767a0b5969fd8600f5732 -------------------------------------------------------------------------------- /tests/encoding_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import unittest 3 | import os 4 | import sys 5 | import subprocess 6 | import fvid 7 | import ffpb 8 | import tqdm 9 | import platform 10 | 11 | from helper import SkippableTest 12 | from shlex import split 13 | from collections import namedtuple 14 | from functools import reduce 15 | 16 | proc_output = namedtuple('proc_output', 'stdout stderr') 17 | 18 | def pipeline(starter_command, *commands): 19 | if not commands: 20 | try: 21 | starter_command, *commands = starter_command.split('|') 22 | except AttributeError: 23 | pass 24 | starter_command = _parse(starter_command) 25 | starter = subprocess.Popen(starter_command, stdout=subprocess.PIPE) 26 | last_proc = reduce(_create_pipe, map(_parse, commands), starter) 27 | return proc_output(*last_proc.communicate()) 28 | 29 | def _create_pipe(previous, command): 30 | proc = subprocess.Popen(command, stdin=previous.stdout, stdout=subprocess.PIPE) 31 | previous.stdout.close() 32 | return proc 33 | 34 | def _parse(cmd): 35 | try: 36 | ok = list(map(str, split(cmd))) 37 | print(ok) 38 | return ok 39 | except Exception: 40 | return cmd 41 | 42 | class FvidTestCase(unittest.TestCase): 43 | def test_decode_from_yt(self): 44 | logging_info = "" 45 | encoded_yt_videos = [i for i in sorted(os.listdir('../tests/encoded/'), reverse=True) if i.lower().startswith('yt')] 46 | for video_name in encoded_yt_videos: 47 | cmd = "python3 -m" + video_name[2:-4] + " -i '../tests/encoded/" + video_name + "'" 48 | if platform.system().lower() in ("linux", "darwin"): 49 | cmd += ' -o /dev/null' 50 | elif platform.system().lower() in ("windows",): 51 | cmd += ' -o nul' 52 | resp = pipeline(cmd)[0].decode('utf-8') 53 | print(resp) 54 | logging_info += resp 55 | assert 'error' not in logging_info.lower() 56 | 57 | def test_encode_and_decode_files(self): 58 | logging_info = "" 59 | files = [i for i in sorted(os.listdir('../tests/decoded/'), reverse=True) if not i == 'README.txt'] 60 | possible_encodings = ['-ey5', '-ey5 -f 3', '-eyz5', '-eyz'] 61 | for file_name in files: 62 | for encoding in possible_encodings: 63 | cmd = "python3 -m fvid " + encoding + " -i ../tests/decoded/" + file_name + " -o '../tests/temp/yt_fvid_" + encoding + ".mp4'" 64 | resp = pipeline(cmd)[0].decode('utf-8') 65 | print(resp) 66 | logging_info += resp 67 | break # only doing one for now so we can test decoding 68 | break 69 | videos = [i for i in sorted(os.listdir('./temp/'), reverse=True)] 70 | possible_decodings = ['-dy5', '-dy5 -f 3', '-dyz5', '-dyz'] 71 | for video_name in videos: 72 | for decoding in possible_decodings: 73 | cmd = "python3 -m" + video_name[2:-4].replace('_', ' ') + " -i ../tests/temp/" + video_name 74 | if platform.system().lower() in ("linux", "darwin"): 75 | cmd += ' -o /dev/null' 76 | elif platform.system().lower() in ("windows",): 77 | cmd += ' -o nul' 78 | resp = pipeline(cmd)[0].decode('utf-8') 79 | print(resp) 80 | logging_info += resp 81 | break 82 | break 83 | print(logging_info.lower()) 84 | assert 'error' not in logging_info.lower() 85 | 86 | 87 | if __name__ == '__main__': 88 | # try: 89 | # print(FvidTestCase().test_decode_from_yt()) 90 | # print(FvidTestCase().test_encode_and_decode_files()) 91 | # except KeyboardInterrupt: 92 | # pass 93 | pytest.main([__file__]) -------------------------------------------------------------------------------- /tests/helper.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class SkippableTest(unittest.TestCase): 5 | def skip(self, msg): 6 | if hasattr(self, 'skipTest'): 7 | return self.skipTest(msg) 8 | return None 9 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | tqdm -------------------------------------------------------------------------------- /tests/temp/yt_fvid_-ey5.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlfredoSequeida/fvid/7f1bdcf8d78b899c0224f89e669b0d85aed6df56/tests/temp/yt_fvid_-ey5.mp4 --------------------------------------------------------------------------------