├── .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 | [](https://GitHub.com/Naereen/StrapDown.js/graphs/commit-activity)
2 | [](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 |
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
--------------------------------------------------------------------------------