├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── build_and_test.sh ├── config.ld ├── doc ├── index.html ├── ldoc.css ├── modules │ ├── ffmpeg.html │ ├── ffmpeg.torch.html │ ├── monad.html │ ├── monad.result.html │ └── result.html └── topics │ ├── LICENSE.html │ └── README.md.html ├── docker-compose.yml ├── rockspecs └── ffmpeg-ffi-scm-0.rockspec ├── src └── ffmpeg │ ├── ffmpeg.lua │ └── torch.lua └── test ├── download_test_data.sh ├── ffmpeg_spec.lua └── ffmpeg_torch_spec.lua /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | *.src.rock 6 | *.zip 7 | *.tar.gz 8 | *.rock 9 | 10 | # Object files 11 | *.o 12 | *.os 13 | *.ko 14 | *.obj 15 | *.elf 16 | 17 | # Precompiled Headers 18 | *.gch 19 | *.pch 20 | 21 | # Libraries 22 | *.lib 23 | *.a 24 | *.la 25 | *.lo 26 | *.def 27 | *.exp 28 | 29 | # Shared objects (inc. Windows DLLs) 30 | *.dll 31 | *.so 32 | *.so.* 33 | *.dylib 34 | 35 | # Executables 36 | *.exe 37 | *.out 38 | *.app 39 | *.i*86 40 | *.x86_64 41 | *.hex 42 | 43 | # Test data 44 | /test/data/ 45 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | 3 | # Install build tools 4 | RUN apt-get update \ 5 | && apt-get install -y build-essential git 6 | 7 | # Install OpenBLAS 8 | RUN apt-get update \ 9 | && apt-get install -y gfortran 10 | RUN git clone https://github.com/xianyi/OpenBLAS.git /tmp/OpenBLAS \ 11 | && cd /tmp/OpenBLAS \ 12 | && [ $(getconf _NPROCESSORS_ONLN) = 1 ] && export USE_OPENMP=0 || export USE_OPENMP=1 \ 13 | && make NO_AFFINITY=1 \ 14 | && make install \ 15 | && rm -rf /tmp/OpenBLAS 16 | 17 | # Install Torch 18 | RUN apt-get update \ 19 | && apt-get install -y cmake curl unzip libreadline-dev libjpeg-dev \ 20 | libpng-dev ncurses-dev imagemagick gnuplot gnuplot-x11 libssl-dev \ 21 | libzmq3-dev graphviz 22 | RUN git clone https://github.com/torch/distro.git ~/torch --recursive \ 23 | && cd ~/torch \ 24 | && ./install.sh 25 | 26 | # Export environment variables manually 27 | ENV LUA_PATH='/root/.luarocks/share/lua/5.1/?.lua;/root/.luarocks/share/lua/5.1/?/init.lua;/root/torch/install/share/lua/5.1/?.lua;/root/torch/install/share/lua/5.1/?/init.lua;./?.lua;/root/torch/install/share/luajit-2.1.0-alpha/?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua' \ 28 | LUA_CPATH='/root/.luarocks/lib/lua/5.1/?.so;/root/torch/install/lib/lua/5.1/?.so;./?.so;/usr/local/lib/lua/5.1/?.so;/usr/local/lib/lua/5.1/loadall.so' \ 29 | PATH=/root/torch/install/bin:$PATH \ 30 | LD_LIBRARY_PATH=/root/torch/install/lib:$LD_LIBRARY_PATH \ 31 | DYLD_LIBRARY_PATH=/root/torch/install/lib:$DYLD_LIBRARY_PATH 32 | 33 | # Install required dependencies for ffmpeg.lua 34 | RUN echo "deb http://ppa.launchpad.net/kirillshkrogalev/ffmpeg-next/ubuntu trusty main" \ 35 | > /etc/apt/sources.list.d/ffmpeg.list \ 36 | && apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 8EFE5982 37 | RUN apt-get update \ 38 | && apt-get install -y \ 39 | cpp \ 40 | libavformat-ffmpeg-dev \ 41 | libavcodec-ffmpeg-dev \ 42 | libavutil-ffmpeg-dev \ 43 | libavfilter-ffmpeg-dev 44 | 45 | # Clean up 46 | RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 47 | 48 | # Install busted for running tests 49 | RUN luarocks install busted 50 | 51 | # Make working directory for this project 52 | RUN mkdir -p /app/test 53 | WORKDIR /app 54 | 55 | # Download test data 56 | COPY ./test/download_test_data.sh /app/test/ 57 | RUN cd test && ./download_test_data.sh 58 | 59 | COPY ./test/* /app/test/ 60 | COPY ./src /app/src 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Aiden Nibali 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ffmpeg-ffi 2 | 3 | LuaJIT FFI bindings to FFmpeg libraries. 4 | 5 | ## Usage 6 | 7 | local ffmpeg = require('ffmpeg') 8 | 9 | local ascii_frame = ffmpeg.new('./example.mpg') 10 | :filter('gray', 'scale=40:12') 11 | :read_video_frame() 12 | :to_ascii() 13 | 14 | print(ascii_frame) 15 | 16 | ### Torch 17 | 18 | If you have Torch installed, load the enhanced version of the library. 19 | 20 | local ffmpeg = require('ffmpeg.torch') 21 | 22 | local byte_tensor = ffmpeg.new('./example.mpg') 23 | :filter('rgb24', 'scale=512:512') 24 | :read_video_frame() 25 | :to_byte_tensor() 26 | 27 | local float_tensor = first_frame_tensor:float():div(255) 28 | -------------------------------------------------------------------------------- /build_and_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | docker-compose stop 4 | docker-compose rm -f 5 | docker-compose build 6 | docker-compose run --rm ffmpeg-lua busted test 7 | -------------------------------------------------------------------------------- /config.ld: -------------------------------------------------------------------------------- 1 | title = 'ffmpeg-ffi docs' 2 | project = 'ffmpeg-ffi' 3 | file = {'src/'} 4 | topics = {'README.md', 'LICENSE'} 5 | format = 'markdown' 6 | -------------------------------------------------------------------------------- /doc/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | ffmpeg-ffi docs 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 44 | 45 |
46 | 47 | 48 | 49 |

Modules

50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
ffmpegLua bindings to FFmpeg libraries.
ffmpeg.torchTorch utilities for Lua bindings to FFmpeg libraries.
60 |

Topics

61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
README.md
LICENSE
71 | 72 |
73 |
74 |
75 | generated by LDoc 1.4.3 76 | Last updated 2015-12-23 12:05:16 77 |
78 |
79 | 80 | 81 | -------------------------------------------------------------------------------- /doc/ldoc.css: -------------------------------------------------------------------------------- 1 | /* BEGIN RESET 2 | 3 | Copyright (c) 2010, Yahoo! Inc. All rights reserved. 4 | Code licensed under the BSD License: 5 | http://developer.yahoo.com/yui/license.html 6 | version: 2.8.2r1 7 | */ 8 | html { 9 | color: #000; 10 | background: #FFF; 11 | } 12 | body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td { 13 | margin: 0; 14 | padding: 0; 15 | } 16 | table { 17 | border-collapse: collapse; 18 | border-spacing: 0; 19 | } 20 | fieldset,img { 21 | border: 0; 22 | } 23 | address,caption,cite,code,dfn,em,strong,th,var,optgroup { 24 | font-style: inherit; 25 | font-weight: inherit; 26 | } 27 | del,ins { 28 | text-decoration: none; 29 | } 30 | li { 31 | list-style: disc; 32 | margin-left: 20px; 33 | } 34 | caption,th { 35 | text-align: left; 36 | } 37 | h1,h2,h3,h4,h5,h6 { 38 | font-size: 100%; 39 | font-weight: bold; 40 | } 41 | q:before,q:after { 42 | content: ''; 43 | } 44 | abbr,acronym { 45 | border: 0; 46 | font-variant: normal; 47 | } 48 | sup { 49 | vertical-align: baseline; 50 | } 51 | sub { 52 | vertical-align: baseline; 53 | } 54 | legend { 55 | color: #000; 56 | } 57 | input,button,textarea,select,optgroup,option { 58 | font-family: inherit; 59 | font-size: inherit; 60 | font-style: inherit; 61 | font-weight: inherit; 62 | } 63 | input,button,textarea,select {*font-size:100%; 64 | } 65 | /* END RESET */ 66 | 67 | body { 68 | margin-left: 1em; 69 | margin-right: 1em; 70 | font-family: arial, helvetica, geneva, sans-serif; 71 | background-color: #ffffff; margin: 0px; 72 | } 73 | 74 | code, tt { font-family: monospace; font-size: 1.1em; } 75 | span.parameter { font-family:monospace; } 76 | span.parameter:after { content:":"; } 77 | span.types:before { content:"("; } 78 | span.types:after { content:")"; } 79 | .type { font-weight: bold; font-style:italic } 80 | 81 | body, p, td, th { font-size: .95em; line-height: 1.2em;} 82 | 83 | p, ul { margin: 10px 0 0 0px;} 84 | 85 | strong { font-weight: bold;} 86 | 87 | em { font-style: italic;} 88 | 89 | h1 { 90 | font-size: 1.5em; 91 | margin: 0 0 20px 0; 92 | } 93 | h2, h3, h4 { margin: 15px 0 10px 0; } 94 | h2 { font-size: 1.25em; } 95 | h3 { font-size: 1.15em; } 96 | h4 { font-size: 1.06em; } 97 | 98 | a:link { font-weight: bold; color: #004080; text-decoration: none; } 99 | a:visited { font-weight: bold; color: #006699; text-decoration: none; } 100 | a:link:hover { text-decoration: underline; } 101 | 102 | hr { 103 | color:#cccccc; 104 | background: #00007f; 105 | height: 1px; 106 | } 107 | 108 | blockquote { margin-left: 3em; } 109 | 110 | ul { list-style-type: disc; } 111 | 112 | p.name { 113 | font-family: "Andale Mono", monospace; 114 | padding-top: 1em; 115 | } 116 | 117 | pre { 118 | background-color: rgb(245, 245, 245); 119 | border: 1px solid #C0C0C0; /* silver */ 120 | padding: 10px; 121 | margin: 10px 0 10px 0; 122 | overflow: auto; 123 | font-family: "Andale Mono", monospace; 124 | } 125 | 126 | pre.example { 127 | font-size: .85em; 128 | } 129 | 130 | table.index { border: 1px #00007f; } 131 | table.index td { text-align: left; vertical-align: top; } 132 | 133 | #container { 134 | margin-left: 1em; 135 | margin-right: 1em; 136 | background-color: #f0f0f0; 137 | } 138 | 139 | #product { 140 | text-align: center; 141 | border-bottom: 1px solid #cccccc; 142 | background-color: #ffffff; 143 | } 144 | 145 | #product big { 146 | font-size: 2em; 147 | } 148 | 149 | #main { 150 | background-color: #f0f0f0; 151 | border-left: 2px solid #cccccc; 152 | } 153 | 154 | #navigation { 155 | float: left; 156 | width: 14em; 157 | vertical-align: top; 158 | background-color: #f0f0f0; 159 | overflow: visible; 160 | } 161 | 162 | #navigation h2 { 163 | background-color:#e7e7e7; 164 | font-size:1.1em; 165 | color:#000000; 166 | text-align: left; 167 | padding:0.2em; 168 | border-top:1px solid #dddddd; 169 | border-bottom:1px solid #dddddd; 170 | } 171 | 172 | #navigation ul 173 | { 174 | font-size:1em; 175 | list-style-type: none; 176 | margin: 1px 1px 10px 1px; 177 | } 178 | 179 | #navigation li { 180 | text-indent: -1em; 181 | display: block; 182 | margin: 3px 0px 0px 22px; 183 | } 184 | 185 | #navigation li li a { 186 | margin: 0px 3px 0px -1em; 187 | } 188 | 189 | #content { 190 | margin-left: 14em; 191 | padding: 1em; 192 | width: 700px; 193 | border-left: 2px solid #cccccc; 194 | border-right: 2px solid #cccccc; 195 | background-color: #ffffff; 196 | } 197 | 198 | #about { 199 | clear: both; 200 | padding: 5px; 201 | border-top: 2px solid #cccccc; 202 | background-color: #ffffff; 203 | } 204 | 205 | @media print { 206 | body { 207 | font: 12pt "Times New Roman", "TimeNR", Times, serif; 208 | } 209 | a { font-weight: bold; color: #004080; text-decoration: underline; } 210 | 211 | #main { 212 | background-color: #ffffff; 213 | border-left: 0px; 214 | } 215 | 216 | #container { 217 | margin-left: 2%; 218 | margin-right: 2%; 219 | background-color: #ffffff; 220 | } 221 | 222 | #content { 223 | padding: 1em; 224 | background-color: #ffffff; 225 | } 226 | 227 | #navigation { 228 | display: none; 229 | } 230 | pre.example { 231 | font-family: "Andale Mono", monospace; 232 | font-size: 10pt; 233 | page-break-inside: avoid; 234 | } 235 | } 236 | 237 | table.module_list { 238 | border-width: 1px; 239 | border-style: solid; 240 | border-color: #cccccc; 241 | border-collapse: collapse; 242 | } 243 | table.module_list td { 244 | border-width: 1px; 245 | padding: 3px; 246 | border-style: solid; 247 | border-color: #cccccc; 248 | } 249 | table.module_list td.name { background-color: #f0f0f0; min-width: 200px; } 250 | table.module_list td.summary { width: 100%; } 251 | 252 | 253 | table.function_list { 254 | border-width: 1px; 255 | border-style: solid; 256 | border-color: #cccccc; 257 | border-collapse: collapse; 258 | } 259 | table.function_list td { 260 | border-width: 1px; 261 | padding: 3px; 262 | border-style: solid; 263 | border-color: #cccccc; 264 | } 265 | table.function_list td.name { background-color: #f0f0f0; min-width: 200px; } 266 | table.function_list td.summary { width: 100%; } 267 | 268 | ul.nowrap { 269 | overflow:auto; 270 | white-space:nowrap; 271 | } 272 | 273 | dl.table dt, dl.function dt {border-top: 1px solid #ccc; padding-top: 1em;} 274 | dl.table dd, dl.function dd {padding-bottom: 1em; margin: 10px 0 0 20px;} 275 | dl.table h3, dl.function h3 {font-size: .95em;} 276 | 277 | /* stop sublists from having initial vertical space */ 278 | ul ul { margin-top: 0px; } 279 | ol ul { margin-top: 0px; } 280 | ol ol { margin-top: 0px; } 281 | ul ol { margin-top: 0px; } 282 | 283 | /* make the target distinct; helps when we're navigating to a function */ 284 | a:target + * { 285 | background-color: #FF9; 286 | } 287 | 288 | 289 | /* styles for prettification of source */ 290 | pre .comment { color: #558817; } 291 | pre .constant { color: #a8660d; } 292 | pre .escape { color: #844631; } 293 | pre .keyword { color: #aa5050; font-weight: bold; } 294 | pre .library { color: #0e7c6b; } 295 | pre .marker { color: #512b1e; background: #fedc56; font-weight: bold; } 296 | pre .string { color: #8080ff; } 297 | pre .number { color: #f8660d; } 298 | pre .operator { color: #2239a8; font-weight: bold; } 299 | pre .preprocessor, pre .prepro { color: #a33243; } 300 | pre .global { color: #800080; } 301 | pre .user-keyword { color: #800080; } 302 | pre .prompt { color: #558817; } 303 | pre .url { color: #272fc2; text-decoration: underline; } 304 | 305 | -------------------------------------------------------------------------------- /doc/modules/ffmpeg.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | ffmpeg-ffi docs 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 53 | 54 |
55 | 56 |

Module ffmpeg

57 |

Lua bindings to FFmpeg libraries.

58 |

59 | 60 |

61 |

Info:

62 |
    63 |
  • Copyright: Aiden Nibali 2015
  • 64 |
  • License: MIT
  • 65 |
  • Author: Aiden Nibali
  • 66 |
67 | 68 | 69 |

Functions

70 | 71 | 72 | 73 | 74 | 75 |
new (path)Opens a video file for reading.
76 |

Class Video

77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 |
Video:filter (pixel_format_name[, filterchain='null'])Sets a filter to apply to the video.
Video:duration ()Gets the video duration in seconds.
Video:pixel_format_name ()Gets the name of the video pixel format.
Video:read_video_frame ()Reads the next video frame.
95 |

Class VideoFrame

96 | 97 | 98 | 99 | 100 | 101 |
VideoFrame:to_ascii ()Converts the video frame to an ASCII visualisation.
102 | 103 |
104 |
105 | 106 | 107 |

Functions

108 | 109 |
110 |
111 | 112 | new (path) 113 |
114 |
115 | Opens a video file for reading. 116 | 117 | 118 |

Parameters:

119 |
    120 |
  • path 121 | string 122 | A relative or absolute path to the video file. 123 |
  • 124 |
125 | 126 |

Returns:

127 |
    128 | 129 | Video 130 | 131 | 132 | 133 |
134 | 135 | 136 | 137 | 138 |
139 |
140 |

Class Video

141 | 142 |
143 | A Video class. 144 |
145 |
146 |
147 | 148 | Video:filter (pixel_format_name[, filterchain='null']) 149 |
150 |
151 | Sets a filter to apply to the video. 152 | 153 | 154 |

Parameters:

155 |
    156 |
  • pixel_format_name 157 | string 158 | The name of the desired output pixel format. 159 | Pixel names can be found in 160 | pixdesc.c. 161 |
  • 162 |
  • filterchain 163 | string 164 | The filterchain to be applied. Refer to the 165 | libav documentation 166 | for the syntax of this string. 167 | (default 'null') 168 |
  • 169 |
170 | 171 |

Returns:

172 |
    173 | 174 | Video 175 | A copy of this Video with the specified filter set 176 | up. 177 |
178 | 179 | 180 | 181 |

Usage:

182 |
    183 |
    184 |  -- Set up a filter which scales the video to 128x128 pixels, flips it
    185 |  -- horizontally and sets the output pixel format to 24-bit RGB:
    186 |  video = video:filter('rgb24', 'scale=128x128,hflip')
    187 |
188 | 189 |
190 |
191 | 192 | Video:duration () 193 |
194 |
195 | Gets the video duration in seconds. 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 |
204 |
205 | 206 | Video:pixel_format_name () 207 |
208 |
209 | Gets the name of the video pixel format. 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 |
218 |
219 | 220 | Video:read_video_frame () 221 |
222 |
223 | Reads the next video frame. 224 | 225 | 226 | 227 |

Returns:

228 |
    229 | 230 | VideoFrame 231 | 232 | 233 | 234 |
235 | 236 | 237 | 238 | 239 |
240 |
241 |

Class VideoFrame

242 | 243 |
244 | A VideoFrame class. 245 |
246 |
247 |
248 | 249 | VideoFrame:to_ascii () 250 |
251 |
252 | Converts the video frame to an ASCII visualisation. 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 |
261 |
262 | 263 | 264 |
265 |
266 |
267 | generated by LDoc 1.4.3 268 | Last updated 2015-12-23 12:05:16 269 |
270 |
271 | 272 | 273 | -------------------------------------------------------------------------------- /doc/modules/ffmpeg.torch.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | ffmpeg-ffi docs 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 51 | 52 |
53 | 54 |

Module ffmpeg.torch

55 |

Torch utilities for Lua bindings to FFmpeg libraries.

56 |

The Torch module must 57 | be installed for ffmpeg.torch to work.

58 |

Info:

59 |
    60 |
  • Copyright: Aiden Nibali 2015
  • 61 |
  • License: MIT
  • 62 |
  • Author: Aiden Nibali
  • 63 |
64 | 65 | 66 |

Class VideoFrame

67 | 68 | 69 | 70 | 71 | 72 |
VideoFrame:to_byte_tensor ()Converts the video frame to a Torch tensor.
73 | 74 |
75 |
76 | 77 | 78 |

Class VideoFrame

79 | 80 |
81 | A VideoFrame class. 82 |
83 |
84 |
85 | 86 | VideoFrame:to_byte_tensor () 87 |
88 |
89 | Converts the video frame to a Torch tensor. 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
98 |
99 | 100 | 101 |
102 |
103 |
104 | generated by LDoc 1.4.3 105 | Last updated 2015-12-23 12:05:16 106 |
107 |
108 | 109 | 110 | -------------------------------------------------------------------------------- /doc/modules/monad.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | ffmpeg-ffi docs 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 49 | 50 |
51 | 52 |

Module monad

53 |

Monad stuffs.

54 |

Some code is based on 55 | Douglas Crockford's JavaScript monads.

56 |

Info:

57 |
    58 |
  • Copyright: Aiden Nibali 2015
  • 59 |
  • License: MIT
  • 60 |
  • Author: Aiden Nibali
  • 61 |
62 | 63 | 64 | 65 |
66 |
67 | 68 | 69 | 70 | 71 |
72 |
73 |
74 | generated by LDoc 1.4.3 75 | Last updated 2015-12-21 10:51:14 76 |
77 |
78 | 79 | 80 | -------------------------------------------------------------------------------- /doc/modules/monad.result.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | ffmpeg-ffi docs 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 55 | 56 |
57 | 58 |

Module monad.result

59 |

A theoretical union type for Value and Error.

60 |

For example, if a function is documented as returning a string Result, 61 | this means that it returns a Value wrapping a string on success, or an 62 | Error otherwise.

63 | 64 |

Here's an example of using a function that returns a Result:

65 | 66 | 67 |
 68 | some_function():and_then(function(value)
 69 |   print('some_function returned ' .. value)
 70 | end):catch(function(error_message))
 71 |   print('Error calling some_function: ' .. error_message)
 72 | end)
 73 | 
74 | 75 |

However, the real beauty of this approach comes with chaining calls:

76 | 77 | 78 |
 79 | create_a_shape()
 80 |   :scale(2)
 81 |   :rotate(45)
 82 |   :translate(10, 5)
 83 |   :catch(function(error_message))
 84 |     print('Error creating and transforming shape: ' .. error_message)
 85 |   end)
 86 | 
87 | 88 |

In the above example we assume that createashape() returns a Result. 89 | If any of the steps along the way fails, no further methods will be called 90 | and the catch function will be triggered. If all of the steps are successful, 91 | the catch function will not be called.

92 | 93 |

At some point you may want to unwrap a Value to get the actual value inside 94 | of it. A good pattern for doing this is as follows:

95 | 96 | 97 |
 98 | local my_shape = create_a_shape()
 99 |   :scale(2)
100 |   :rotate(45)
101 |   :translate(10, 5)
102 |   :catch(function(error_message))
103 |     return default_shape
104 |   end)
105 |   :get()
106 | 
107 | 108 |

:get is a special function which returns the value wrapped by a Value. 109 | But be careful! If you call get on an Error then a Lua error will be 110 | raised - this is why in the example above we make sure that we make a 111 | default value available in the case of an Error.

112 |

Info:

113 |
    114 |
  • Copyright: Aiden Nibali 2015
  • 115 |
  • License: MIT
  • 116 |
  • Author: Aiden Nibali
  • 117 |
118 | 119 | 120 |

Functions

121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 |
Error ()Returns a factory for making Error monads.
Value ()Returns a factory for making Value monads.
131 |

Class ErrorMonad

132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 |
m:done (...)Alias for Error:get
m:get ()Rethrows the error.
m:catch (callback)Calls function callback with the error message.
m:and_then (callback)Returns self (callback is not called).
150 |

Class Value

151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 |
m:done (...)Alias for Value:get
m:get ()Returns the wrapped value.
m:catch (callback)Returns self (callback is not called).
m:and_then (callback)Calls function callback with the wrapped value.
169 | 170 |
171 |
172 | 173 | 174 |

Functions

175 | 176 |
177 |
178 | 179 | Error () 180 |
181 |
182 | Returns a factory for making Error monads. 183 | 184 | 185 | 186 | 187 | 188 | 189 |

Usage:

190 |
    191 |
    192 |  local error_factory = monad.Error()
    193 |  local error_monad = error_factory('An error occurred!')
    194 |
195 | 196 |
197 |
198 | 199 | Value () 200 |
201 |
202 | Returns a factory for making Value monads. 203 | 204 | 205 | 206 | 207 | 208 | 209 |

Usage:

210 |
    211 |
    212 |  local value_factory = monad.Value()
    213 |  local value_monad = value_factory(42)
    214 |
215 | 216 |
217 |
218 |

Class ErrorMonad

219 | 220 |
221 | 222 |

A monad which represents an error result.

223 | 224 |

An ErrorMonad will:

225 | 226 |
    227 |
  • Throw an error when :get or :done is called on it.
  • 228 |
  • Call the function callback with the error message when :catch(callback) 229 | is called on it.
  • 230 |
  • Return itself in response to all other method calls.
  • 231 |
232 | 233 |
234 |
235 |
236 | 237 | m:done (...) 238 |
239 |
240 | Alias for Error:get 241 | 242 | 243 |

Parameters:

244 |
    245 |
  • ... 246 | 247 | 248 | 249 |
  • 250 |
251 | 252 | 253 | 254 |

See also:

255 |
    256 |
257 | 258 | 259 |
260 |
261 | 262 | m:get () 263 |
264 |
265 | Rethrows the error. 266 | 267 | 268 | 269 |

Returns:

270 |
    271 | 272 | Nothing, since this method always throws an error. 273 |
274 | 275 | 276 | 277 | 278 |
279 |
280 | 281 | m:catch (callback) 282 |
283 |
284 | Calls function callback with the error message. 285 | 286 | 287 |

Parameters:

288 |
    289 |
  • callback 290 | func 291 | 292 | 293 | 294 |
  • 295 |
296 | 297 |

Returns:

298 |
    299 | 300 | Result 301 | The result of calling callback. 302 |
303 | 304 | 305 | 306 | 307 |
308 |
309 | 310 | m:and_then (callback) 311 |
312 |
313 | Returns self (callback is not called). 314 | 315 | 316 |

Parameters:

317 |
    318 |
  • callback 319 | func 320 | 321 | 322 | 323 |
  • 324 |
325 | 326 |

Returns:

327 |
    328 | 329 | ErrorMonad 330 | Self. 331 |
332 | 333 | 334 | 335 | 336 |
337 |
338 |

Class Value

339 | 340 |
341 | 342 |

A monad which represents a successful result value.

343 | 344 |

A Value monad will:

345 | 346 |
    347 |
  • Return its wrapped value when :get or :done is called on it.
  • 348 |
  • Call the function callback with the wrapped value when 349 | :and_then(callback) is called on it.
  • 350 |
  • Return itself in response to :catch().
  • 351 |
  • Delegate to the wrapped value in response to all other method calls and 352 | present the return value as a Result.
  • 353 |
354 | 355 | 356 |
357 |
358 |
359 | 360 | m:done (...) 361 |
362 |
363 | Alias for Value:get 364 | 365 | 366 |

Parameters:

367 |
    368 |
  • ... 369 | 370 | 371 | 372 |
  • 373 |
374 | 375 | 376 | 377 |

See also:

378 |
    379 |
380 | 381 | 382 |
383 |
384 | 385 | m:get () 386 |
387 |
388 | Returns the wrapped value. 389 | 390 | 391 | 392 |

Returns:

393 |
    394 | 395 | The wrapped value. 396 |
397 | 398 | 399 | 400 | 401 |
402 |
403 | 404 | m:catch (callback) 405 |
406 |
407 | Returns self (callback is not called). 408 | 409 | 410 |

Parameters:

411 |
    412 |
  • callback 413 | func 414 | 415 | 416 | 417 |
  • 418 |
419 | 420 |

Returns:

421 |
    422 | 423 | Value 424 | Self. 425 |
426 | 427 | 428 | 429 | 430 |
431 |
432 | 433 | m:and_then (callback) 434 |
435 |
436 | Calls function callback with the wrapped value. 437 | 438 | 439 |

Parameters:

440 |
    441 |
  • callback 442 | func 443 | 444 | 445 | 446 |
  • 447 |
448 | 449 |

Returns:

450 |
    451 | 452 | Result 453 | The result of calling callback. 454 |
455 | 456 | 457 | 458 | 459 |
460 |
461 | 462 | 463 |
464 |
465 |
466 | generated by LDoc 1.4.3 467 | Last updated 2015-12-21 10:51:14 468 |
469 |
470 | 471 | 472 | -------------------------------------------------------------------------------- /doc/modules/result.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | ffmpeg-ffi docs 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 55 | 56 |
57 | 58 |

Module result

59 |

A theoretical union type for Value and Error.

60 |

For example, if a function is documented as returning a string Result, 61 | this means that it returns a Value wrapping a string on success, or an 62 | Error otherwise.

63 | 64 |

Here's an example of using a function that returns a Result:

65 | 66 | 67 |
 68 | some_function():and_then(function(value)
 69 |   print('some_function returned ' .. value)
 70 | end):catch(function(error_message))
 71 |   print('Error calling some_function: ' .. error_message)
 72 | end)
 73 | 
74 | 75 |

However, the real beauty of this approach comes with chaining calls:

76 | 77 | 78 |
 79 | create_a_shape()
 80 |   :scale(2)
 81 |   :rotate(45)
 82 |   :translate(10, 5)
 83 |   :catch(function(error_message))
 84 |     print('Error creating and transforming shape: ' .. error_message)
 85 |   end)
 86 | 
87 | 88 |

In the above example we assume that createashape() returns a Result. 89 | If any of the steps along the way fails, no further methods will be called 90 | and the catch function will be triggered. If all of the steps are successful, 91 | the catch function will not be called.

92 | 93 |

At some point you may want to unwrap a Value to get the actual value inside 94 | of it. A good pattern for doing this is as follows:

95 | 96 | 97 |
 98 | local my_shape = create_a_shape()
 99 |   :scale(2)
100 |   :rotate(45)
101 |   :translate(10, 5)
102 |   :catch(function(error_message))
103 |     return default_shape
104 |   end)
105 |   :get()
106 | 
107 | 108 |

:get is a special function which returns the value wrapped by a Value. 109 | But be careful! If you call get on an Error then a Lua error will be 110 | raised - this is why in the example above we make sure that we make a 111 | default value available in the case of an Error.

112 |

Info:

113 |
    114 |
  • Copyright: Aiden Nibali 2015
  • 115 |
  • License: MIT
  • 116 |
  • Author: Aiden Nibali
  • 117 |
118 | 119 | 120 |

Functions

121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 |
Error ()Returns a factory for making Error monads.
Value ()Returns a factory for making Value monads.
131 |

Class Error

132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 |
monad:done (...)Alias for Error:get
monad:get ()Rethrows the error.
monad:catch (callback)Calls function callback with the error message.
monad:and_then (callback)Returns self (callback is not called).
150 |

Class Value

151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 |
monad:done (...)Alias for Value:get
monad:get ()Returns the wrapped value.
monad:catch (callback)Returns self (callback is not called).
monad:and_then (callback)Calls function callback with the wrapped value.
169 | 170 |
171 |
172 | 173 | 174 |

Functions

175 | 176 |
177 |
178 | 179 | Error () 180 |
181 |
182 | 183 |

Returns a factory for making Error monads.

184 | 185 |
local error_factory = monad.Error()
186 | local error_monad = error_factory('An error occurred!')
187 | 
188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 |
197 |
198 | 199 | Value () 200 |
201 |
202 | 203 |

Returns a factory for making Value monads.

204 | 205 |
local value_factory = monad.Value()
206 | local value_monad = value_factory(42)
207 | 
208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 |
217 |
218 |

Class Error

219 | 220 |
221 | 222 |

A monad which represents an error result.

223 | 224 |

An Error monad will:

225 | 226 |
    227 |
  • Throw an error when :get or :done is called on it.
  • 228 |
  • Call the function callback with the error message when :catch(callback) 229 | is called on it.
  • 230 |
  • Return itself in response to all other method calls.
  • 231 |
232 | 233 |
234 |
235 |
236 | 237 | monad:done (...) 238 |
239 |
240 | Alias for Error:get 241 | 242 | 243 |

Parameters:

244 |
    245 |
  • ... 246 | 247 | 248 | 249 |
  • 250 |
251 | 252 | 253 | 254 |

See also:

255 |
    256 |
257 | 258 | 259 |
260 |
261 | 262 | monad:get () 263 |
264 |
265 | Rethrows the error. 266 | 267 | 268 | 269 |

Returns:

270 |
    271 | 272 | Nothing, since this method always throws an error. 273 |
274 | 275 | 276 | 277 | 278 |
279 |
280 | 281 | monad:catch (callback) 282 |
283 |
284 | Calls function callback with the error message. 285 | 286 | 287 |

Parameters:

288 |
    289 |
  • callback 290 | func 291 | 292 | 293 | 294 |
  • 295 |
296 | 297 |

Returns:

298 |
    299 | 300 | Result 301 | The result of calling callback. 302 |
303 | 304 | 305 | 306 | 307 |
308 |
309 | 310 | monad:and_then (callback) 311 |
312 |
313 | Returns self (callback is not called). 314 | 315 | 316 |

Parameters:

317 |
    318 |
  • callback 319 | func 320 | 321 | 322 | 323 |
  • 324 |
325 | 326 |

Returns:

327 |
    328 | 329 | Error 330 | Self. 331 |
332 | 333 | 334 | 335 | 336 |
337 |
338 |

Class Value

339 | 340 |
341 | 342 |

A monad which represents a successful result value.

343 | 344 |

A Value monad will:

345 | 346 |
    347 |
  • Return its wrapped value when :get or :done is called on it.
  • 348 |
  • Call the function callback with the wrapped value when 349 | :and_then(callback) is called on it.
  • 350 |
  • Return itself in response to :catch().
  • 351 |
  • Delegate to the wrapped value in response to all other method calls and 352 | present the return value as a Result.
  • 353 |
354 | 355 | 356 |
357 |
358 |
359 | 360 | monad:done (...) 361 |
362 |
363 | Alias for Value:get 364 | 365 | 366 |

Parameters:

367 |
    368 |
  • ... 369 | 370 | 371 | 372 |
  • 373 |
374 | 375 | 376 | 377 |

See also:

378 |
    379 |
380 | 381 | 382 |
383 |
384 | 385 | monad:get () 386 |
387 |
388 | Returns the wrapped value. 389 | 390 | 391 | 392 |

Returns:

393 |
    394 | 395 | The wrapped value. 396 |
397 | 398 | 399 | 400 | 401 |
402 |
403 | 404 | monad:catch (callback) 405 |
406 |
407 | Returns self (callback is not called). 408 | 409 | 410 |

Parameters:

411 |
    412 |
  • callback 413 | func 414 | 415 | 416 | 417 |
  • 418 |
419 | 420 |

Returns:

421 |
    422 | 423 | Value 424 | Self. 425 |
426 | 427 | 428 | 429 | 430 |
431 |
432 | 433 | monad:and_then (callback) 434 |
435 |
436 | Calls function callback with the wrapped value. 437 | 438 | 439 |

Parameters:

440 |
    441 |
  • callback 442 | func 443 | 444 | 445 | 446 |
  • 447 |
448 | 449 |

Returns:

450 |
    451 | 452 | Result 453 | The result of calling callback. 454 |
455 | 456 | 457 | 458 | 459 |
460 |
461 | 462 | 463 |
464 |
465 |
466 | generated by LDoc 1.4.3 467 | Last updated 2015-12-21 10:24:19 468 |
469 |
470 | 471 | 472 | -------------------------------------------------------------------------------- /doc/topics/LICENSE.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | ffmpeg-ffi docs 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 47 | 48 |
49 | 50 | The MIT License (MIT)

51 | 52 |

Copyright (c) 2015 Aiden Nibali

53 | 54 |

Permission is hereby granted, free of charge, to any person obtaining a copy 55 | of this software and associated documentation files (the "Software"), to deal 56 | in the Software without restriction, including without limitation the rights 57 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 58 | copies of the Software, and to permit persons to whom the Software is 59 | furnished to do so, subject to the following conditions:

60 | 61 |

The above copyright notice and this permission notice shall be included in all 62 | copies or substantial portions of the Software.

63 | 64 |

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 65 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 66 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 67 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 68 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 69 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 70 | SOFTWARE. 71 | 72 |

73 |
74 |
75 | generated by LDoc 1.4.3 76 | Last updated 2015-12-23 12:05:16 77 |
78 |
79 | 80 | 81 | -------------------------------------------------------------------------------- /doc/topics/README.md.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | ffmpeg-ffi docs 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 51 | 52 |
53 | 54 | 55 |

ffmpeg-ffi

56 | 57 |

LuaJIT FFI bindings to FFmpeg libraries.

58 | 59 |

60 |

Usage

61 | 62 | 63 |
 64 | local ffmpeg = require('ffmpeg')
 65 | 
 66 | ffmpeg.new('./example.mpg')
 67 |   :filter('gray', 'scale=40:12')
 68 |   :read_video_frame()
 69 |   :to_ascii()
 70 |   :and_then(function(ascii_frame)
 71 |     print(ascii_frame)
 72 |   end)
 73 |   :catch(function(err)
 74 |     print('An error occurred: ' .. err)
 75 |   end)
 76 | 
77 | 78 |

Torch

79 | 80 |

If you have Torch installed, load the enhanced version of the library.

81 | 82 | 83 |
 84 | local ffmpeg = require('ffmpeg.torch')
 85 | 
 86 | local first_frame_tensor = ffmpeg.new('./example.mpg')
 87 |   :filter('rgb24', 'scale=512:512')
 88 |   :read_video_frame()
 89 |   :to_byte_tensor()
 90 |   :catch(function(err)
 91 |     -- Use the Lena image in case of error
 92 |     return image:lena()
 93 |   end)
 94 |   :get()
 95 | 
96 | 97 | 98 | 99 |
100 |
101 |
102 | generated by LDoc 1.4.3 103 | Last updated 2015-12-23 12:05:16 104 |
105 |
106 | 107 | 108 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | ffmpeg-lua: 2 | build: . 3 | -------------------------------------------------------------------------------- /rockspecs/ffmpeg-ffi-scm-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "ffmpeg-ffi" 2 | version = "scm-0" 3 | 4 | source = { 5 | url = "https://github.com/anibali/lua-ffmpeg-ffi/archive/master.zip", 6 | dir = "lua-ffmpeg-ffi-master" 7 | } 8 | 9 | description = { 10 | summary = "LuaJIT FFI bindings to FFmpeg libraries", 11 | detailed = [[ 12 | This module provides LuaJIT FFI bindings to FFmpeg libraries for processing 13 | videos 14 | ]], 15 | homepage = "https://github.com/anibali/lua-ffmpeg-ffi", 16 | license = "MIT " 17 | } 18 | 19 | dependencies = { 20 | "lua >= 5.1" 21 | } 22 | 23 | build = { 24 | type = "builtin", 25 | modules = { 26 | ["ffmpeg"] = "src/ffmpeg/ffmpeg.lua", 27 | ["ffmpeg.torch"] = "src/ffmpeg/torch.lua" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ffmpeg/ffmpeg.lua: -------------------------------------------------------------------------------- 1 | ------------ 2 | -- Lua bindings to FFmpeg libraries. 3 | -- @module ffmpeg 4 | -- @author Aiden Nibali 5 | -- @license MIT 6 | -- @copyright Aiden Nibali 2015 7 | 8 | local ffi = require('ffi') 9 | 10 | local M = {} 11 | local Video = {} 12 | local VideoFrame = {} 13 | 14 | -- Write includes to a temporary file 15 | local includes_path = os.tmpname() 16 | local includes_file = io.open(includes_path, 'w') 17 | includes_file:write[[ 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | ]] 27 | includes_file:close() 28 | 29 | -- Preprocess header files to get C declarations 30 | local cpp_output = io.popen('cpp -P -w -x c ' .. includes_path) 31 | local def = cpp_output:read('*all') 32 | cpp_output:close() 33 | os.remove(includes_path) 34 | 35 | -- Parse C declarations with FFI 36 | ffi.cdef(def) 37 | 38 | local function load_lib(t) 39 | local err = '' 40 | for _, name in ipairs(t) do 41 | local ok, mod = pcall(ffi.load, name) 42 | if ok then return mod, name end 43 | err = err .. '\n' .. mod 44 | end 45 | error(err) 46 | end 47 | 48 | local libavformat = load_lib{ 49 | 'libavformat-ffmpeg.so.56', 'libavformat.so.56', 'avformat'} 50 | local libavcodec = load_lib{ 51 | 'libavcodec-ffmpeg.so.56', 'libavcodec.so.56', 'avcodec'} 52 | local libavutil = load_lib{ 53 | 'libavutil-ffmpeg.so.54', 'libavutil.so.54', 'avutil'} 54 | local libavfilter = load_lib{ 55 | 'libavfilter-ffmpeg.so.5', 'libavfilter.so.5', 'avfilter'} 56 | 57 | M.libavformat = libavformat 58 | M.libavcodec = libavcodec 59 | M.libavutil = libavutil 60 | M.libavfilter = libavfilter 61 | 62 | local AV_OPT_SEARCH_CHILDREN = 1 63 | 64 | local av_log_level = { 65 | quiet = -8, 66 | panic = 0, 67 | fatal = 8, 68 | error = 16, 69 | warning = 24, 70 | info = 32, 71 | verbose = 40, 72 | debug = 48, 73 | trace = 56 74 | } 75 | 76 | libavutil.av_log_set_level(av_log_level.error) 77 | 78 | -- Initialize libavformat 79 | libavformat.av_register_all() 80 | 81 | -- Initialize libavfilter 82 | libavfilter.avfilter_register_all() 83 | 84 | local function copy_object(original) 85 | local copy 86 | if type(original) == 'table' then 87 | copy = {} 88 | for k, v in pairs(original) do 89 | copy[k] = v 90 | end 91 | setmetatable(copy, getmetatable(original)) 92 | else 93 | copy = original 94 | end 95 | return copy 96 | end 97 | 98 | local function new_video_frame(ffi_frame) 99 | local self = {ffi_frame = ffi_frame} 100 | setmetatable(self, {__index = VideoFrame}) 101 | 102 | return self 103 | end 104 | 105 | local function create_frame_reader(self) 106 | local frame_reader = coroutine.create(function() 107 | local packet = ffi.new('AVPacket[1]') 108 | libavcodec.av_init_packet(packet) 109 | ffi.gc(packet, libavformat.av_packet_unref) 110 | 111 | local frame = ffi.new('AVFrame*[1]', libavutil.av_frame_alloc()) 112 | if frame[0] == 0 then 113 | error('Failed to allocate frame') 114 | end 115 | ffi.gc(frame, function(ptr) 116 | libavutil.av_frame_unref(ptr[0]) 117 | libavutil.av_frame_free(ptr) 118 | end) 119 | 120 | local filtered_frame 121 | if self.is_filtered then 122 | filtered_frame = ffi.new('AVFrame*[1]', libavutil.av_frame_alloc()) 123 | if filtered_frame[0] == 0 then 124 | error('Failed to allocate filtered_frame') 125 | end 126 | ffi.gc(filtered_frame, function(ptr) 127 | libavutil.av_frame_unref(ptr[0]) 128 | libavutil.av_frame_free(ptr) 129 | end) 130 | end 131 | 132 | while libavformat.av_read_frame(self.format_context[0], packet) == 0 do 133 | -- Make sure packet is from video stream 134 | if packet[0].stream_index == self.video_stream_index then 135 | -- Reset fields in frame 136 | libavutil.av_frame_unref(frame[0]) 137 | 138 | local got_frame = ffi.new('int[1]') 139 | if libavcodec.avcodec_decode_video2(self.video_decoder_context, frame[0], got_frame, packet) < 0 then 140 | error('Failed to decode video frame') 141 | end 142 | 143 | if got_frame[0] ~= 0 then 144 | if self.is_filtered then 145 | -- Push the decoded frame into the filtergraph 146 | if libavfilter.av_buffersrc_add_frame_flags(self.buffersrc_context[0], 147 | frame[0], libavfilter.AV_BUFFERSRC_FLAG_KEEP_REF) < 0 148 | then 149 | error('Error while feeding the filtergraph') 150 | end 151 | 152 | -- Pull filtered frames from the filtergraph 153 | libavutil.av_frame_unref(filtered_frame[0]); 154 | while libavfilter.av_buffersink_get_frame(self.buffersink_context[0], filtered_frame[0]) >= 0 do 155 | coroutine.yield(filtered_frame[0], 'video') 156 | end 157 | else 158 | coroutine.yield(frame[0], 'video') 159 | end 160 | end 161 | 162 | else 163 | -- TODO: Audio frames 164 | end 165 | end 166 | end) 167 | 168 | return frame_reader 169 | end 170 | 171 | ---- Opens a video file for reading. 172 | -- 173 | -- @string path A relative or absolute path to the video file. 174 | -- @treturn Video 175 | function M.new(path) 176 | local self = {is_filtered = false} 177 | setmetatable(self, {__index = Video}) 178 | 179 | self.format_context = ffi.new('AVFormatContext*[1]') 180 | if libavformat.avformat_open_input(self.format_context, path, nil, nil) < 0 then 181 | error('Failed to open video input for ' .. path) 182 | end 183 | 184 | -- Release format context when collected by the GC 185 | ffi.gc(self.format_context, libavformat.avformat_close_input) 186 | 187 | -- Calculate info about the stream 188 | if libavformat.avformat_find_stream_info(self.format_context[0], nil) < 0 then 189 | error('Failed to find stream info for ' .. path) 190 | end 191 | 192 | -- Select video stream 193 | local decoder = ffi.new('AVCodec*[1]') 194 | self.video_stream_index = libavformat.av_find_best_stream( 195 | self.format_context[0], libavformat.AVMEDIA_TYPE_VIDEO, -1, -1, decoder, 0) 196 | if self.video_stream_index < 0 then 197 | error('Failed to find video stream for ' .. path) 198 | end 199 | 200 | self.video_decoder_context = self.format_context[0].streams[self.video_stream_index].codec 201 | 202 | if libavcodec.avcodec_open2(self.video_decoder_context, decoder[0], nil) < 0 then 203 | error('Failed to open video decoder') 204 | end 205 | 206 | -- Release decoder context when collected by the GC 207 | ffi.gc(self.video_decoder_context, libavcodec.avcodec_close) 208 | 209 | -- -- Print format info 210 | -- libavformat.av_dump_format(self.format_context[0], 0, path, 0) 211 | 212 | return self 213 | end 214 | 215 | --- A Video class. 216 | -- @type Video 217 | 218 | ---- Sets a filter to apply to the video. 219 | -- 220 | -- @string pixel_format_name The name of the desired output pixel format. 221 | -- Pixel names can be found in 222 | -- [pixdesc.c](https://www.ffmpeg.org/doxygen/1.1/pixdesc_8c_source.html). 223 | -- @string[opt='null'] filterchain The filterchain to be applied. Refer to the 224 | -- [libav documentation](https://libav.org/documentation/libavfilter.html) 225 | -- for the syntax of this string. 226 | -- @treturn Video A copy of this `Video` with the specified filter set 227 | -- up. 228 | -- 229 | -- @usage 230 | -- -- Set up a filter which scales the video to 128x128 pixels, flips it 231 | -- -- horizontally and sets the output pixel format to 24-bit RGB: 232 | -- video = video:filter('rgb24', 'scale=128x128,hflip') 233 | function Video:filter(pixel_format_name, filterchain) 234 | assert(not self.is_filtered) 235 | 236 | local video = copy_object(self) 237 | 238 | filterchain = filterchain or 'null' 239 | local buffersrc = libavfilter.avfilter_get_by_name('buffer'); 240 | local buffersink = libavfilter.avfilter_get_by_name('buffersink'); 241 | local outputs = ffi.new('AVFilterInOut*[1]', libavfilter.avfilter_inout_alloc()); 242 | ffi.gc(outputs, libavfilter.avfilter_inout_free) 243 | local inputs = ffi.new('AVFilterInOut*[1]', libavfilter.avfilter_inout_alloc()); 244 | ffi.gc(inputs, libavfilter.avfilter_inout_free) 245 | 246 | local filter_graph = ffi.new('AVFilterGraph*[1]', libavfilter.avfilter_graph_alloc()); 247 | ffi.gc(filter_graph, libavfilter.avfilter_graph_free) 248 | 249 | local args = string.format( 250 | 'video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d', 251 | video.video_decoder_context.width, 252 | video.video_decoder_context.height, 253 | tonumber(video.video_decoder_context.pix_fmt), 254 | video.video_decoder_context.time_base.num, 255 | video.video_decoder_context.time_base.den, 256 | video.video_decoder_context.sample_aspect_ratio.num, 257 | video.video_decoder_context.sample_aspect_ratio.den) 258 | 259 | local buffersrc_context = ffi.new('AVFilterContext*[1]'); 260 | if libavfilter.avfilter_graph_create_filter( 261 | buffersrc_context, buffersrc, 'in', args, nil, filter_graph[0]) < 0 262 | then 263 | error('Failed to create buffer source') 264 | end 265 | 266 | local buffersink_context = ffi.new('AVFilterContext*[1]'); 267 | if libavfilter.avfilter_graph_create_filter( 268 | buffersink_context, buffersink, 'out', nil, nil, filter_graph[0]) < 0 269 | then 270 | error('Failed to create buffer sink') 271 | end 272 | 273 | local pix_fmt = libavutil.av_get_pix_fmt(pixel_format_name) 274 | if pix_fmt == libavutil.AV_PIX_FMT_NONE then 275 | error('Invalid pixel format name: ' .. pixel_format_name) 276 | end 277 | local pix_fmts = ffi.new('enum AVPixelFormat[1]', {pix_fmt}) 278 | if libavutil.av_opt_set_bin(buffersink_context[0], 279 | 'pix_fmts', ffi.cast('const unsigned char*', pix_fmts), 280 | 1 * ffi.sizeof('enum AVPixelFormat'), AV_OPT_SEARCH_CHILDREN) < 0 281 | then 282 | error('Failed to set output pixel format') 283 | end 284 | 285 | outputs[0].name = libavutil.av_strdup('in'); 286 | outputs[0].filter_ctx = buffersrc_context[0]; 287 | outputs[0].pad_idx = 0; 288 | outputs[0].next = nil; 289 | inputs[0].name = libavutil.av_strdup('out'); 290 | inputs[0].filter_ctx = buffersink_context[0]; 291 | inputs[0].pad_idx = 0; 292 | inputs[0].next = nil; 293 | 294 | if libavfilter.avfilter_graph_parse_ptr(filter_graph[0], filterchain, 295 | inputs, outputs, nil) < 0 296 | then 297 | error('avfilter_graph_parse_ptr failed') 298 | end 299 | 300 | if libavfilter.avfilter_graph_config(filter_graph[0], nil) < 0 then 301 | error('avfilter_graph_config failed') 302 | end 303 | 304 | video.filter_graph = filter_graph 305 | video.buffersrc_context = buffersrc_context 306 | video.buffersink_context = buffersink_context 307 | video.is_filtered = true 308 | 309 | return video 310 | end 311 | 312 | ---- Gets the video duration in seconds. 313 | function Video:duration() 314 | return tonumber(self.format_context[0].duration) / 1000000.0 315 | end 316 | 317 | ---- Gets the name of the video pixel format. 318 | function Video:pixel_format_name() 319 | return ffi.string(libavutil.av_get_pix_fmt_name(self.video_decoder_context.pix_fmt)) 320 | end 321 | 322 | ---- Reads the next video frame. 323 | -- @treturn VideoFrame 324 | function Video:read_video_frame() 325 | self.frame_reader = self.frame_reader or create_frame_reader(self) 326 | 327 | while true do 328 | if coroutine.status(self.frame_reader) == 'dead' then 329 | error('End of stream') 330 | end 331 | 332 | local ok, frame, frame_type = coroutine.resume(self.frame_reader) 333 | 334 | if not ok then 335 | error(frame) 336 | end 337 | 338 | if frame_type == 'video' then 339 | return new_video_frame(frame) 340 | end 341 | end 342 | end 343 | 344 | function Video:each_frame(video_callback, audio_callback) 345 | if audio_callback ~= nil then 346 | error('Audio frames not supported yet') 347 | end 348 | 349 | local running = true 350 | while running do 351 | local ok, res = pcall(self.read_video_frame, self) 352 | if ok then 353 | video_callback(res) 354 | else 355 | running = false 356 | end 357 | end 358 | end 359 | 360 | --- A VideoFrame class. 361 | -- @type VideoFrame 362 | 363 | ---- Converts the video frame to an ASCII visualisation. 364 | function VideoFrame:to_ascii() 365 | local frame = self.ffi_frame 366 | if frame.format ~= libavutil.AV_PIX_FMT_GRAY8 then 367 | error(string.format( 368 | 'Unexpected pixel format "%s", frame_to_ascii requires "%s"', 369 | ffi.string(libavutil.av_get_pix_fmt_name(frame.format)), 370 | ffi.string(libavutil.av_get_pix_fmt_name(libavutil.AV_PIX_FMT_GRAY8)))) 371 | end 372 | 373 | local ascii = {} 374 | 375 | for y = 0, (frame.height - 1) do 376 | for x = 0, (frame.width - 1) do 377 | local luma = frame.data[0][y * frame.linesize[0] + x] 378 | if luma > 200 then 379 | table.insert(ascii, '#') 380 | elseif luma > 150 then 381 | table.insert(ascii, '+') 382 | elseif luma > 100 then 383 | table.insert(ascii, '-') 384 | elseif luma > 50 then 385 | table.insert(ascii, '.') 386 | else 387 | table.insert(ascii, ' ') 388 | end 389 | end 390 | table.insert(ascii, '\n') 391 | end 392 | 393 | return table.concat(ascii, '') 394 | end 395 | 396 | M.Video = Video 397 | M.VideoFrame = VideoFrame 398 | 399 | return M 400 | -------------------------------------------------------------------------------- /src/ffmpeg/torch.lua: -------------------------------------------------------------------------------- 1 | ------------ 2 | -- Torch utilities for Lua bindings to FFmpeg libraries. The Torch module must 3 | -- be installed for `ffmpeg.torch` to work. 4 | -- 5 | -- @module ffmpeg.torch 6 | -- @author Aiden Nibali 7 | -- @license MIT 8 | -- @copyright Aiden Nibali 2015 9 | 10 | local ffmpeg = require('ffmpeg') 11 | local ffi = require('ffi') 12 | local bit = require('bit') 13 | 14 | -- FIXME: Can't put this before ffmpeg require, probably some FFI interference 15 | -- going on 16 | if not pcall(require, 'torch') then 17 | error('Torch module must be installed to use ffmpeg.torch') 18 | end 19 | 20 | local PIX_FMT_PLANAR = 16 21 | local PIX_FMT_RGB = 32 22 | 23 | local VideoFrame = ffmpeg.VideoFrame 24 | 25 | --- A VideoFrame class. 26 | -- @type VideoFrame 27 | 28 | ---- Converts the video frame to a Torch tensor. 29 | function VideoFrame:to_byte_tensor() 30 | local frame = self.ffi_frame 31 | local desc = ffmpeg.libavutil.av_pix_fmt_desc_get(frame.format) 32 | local is_packed_rgb = 33 | bit.band(desc.flags, bit.bor(PIX_FMT_PLANAR, PIX_FMT_RGB)) == PIX_FMT_RGB 34 | 35 | local tensor 36 | 37 | if is_packed_rgb then 38 | -- TODO: Support other packed RGB formats 39 | assert(ffi.string(ffmpeg.libavutil.av_get_pix_fmt_name(frame.format)) == 'rgb24') 40 | 41 | -- Create a Torch tensor to hold the image 42 | tensor = torch.ByteTensor(3, frame.height, frame.width) 43 | 44 | -- Fill the tensor 45 | local ptr = torch.data(tensor) 46 | local pos = 0 47 | local y_max = frame.height - 1 48 | local triple_x_max = (frame.width - 1) * 3 49 | for i=0,2 do 50 | for y=0,y_max do 51 | local offset = y * frame.linesize[0] + i 52 | for triple_x=0,triple_x_max,3 do 53 | ptr[pos] = frame.data[0][offset + triple_x] 54 | pos = pos + 1 55 | end 56 | end 57 | end 58 | else -- Planar 59 | -- Calculate number of channels (eg 3 for YUV, 1 for greyscale) 60 | local n_channels = 4 61 | for i=3,0,-1 do 62 | if frame.linesize[i] == 0 then 63 | n_channels = n_channels - 1 64 | else 65 | break 66 | end 67 | end 68 | 69 | -- Create a Torch tensor to hold the image 70 | tensor = torch.ByteTensor(n_channels, frame.height, frame.width) 71 | 72 | -- Fill the tensor 73 | local ptr = torch.data(tensor) 74 | local pos = 0 75 | local y_max = frame.height - 1 76 | local x_max = frame.width - 1 77 | for i=0,(n_channels-1) do 78 | local stride = frame.linesize[i] 79 | local channel_data = frame.data[i] 80 | for y=0,y_max do 81 | local offset = stride * y 82 | for x=0,x_max do 83 | ptr[pos] = channel_data[offset + x] 84 | pos = pos + 1 85 | end 86 | end 87 | end 88 | end 89 | 90 | return tensor 91 | end 92 | 93 | return ffmpeg 94 | -------------------------------------------------------------------------------- /test/download_test_data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | mkdir -p data 4 | cd data 5 | 6 | if [ -f data_downloaded ]; then 7 | echo "Skipping test data download" 8 | else 9 | echo "Downloading test data..." 10 | curl -O http://hubblesource.stsci.edu/sources/video/clips/details/images/centaur_1.mpg 11 | touch data_downloaded 12 | echo "Test data downloaded successfully" 13 | fi 14 | -------------------------------------------------------------------------------- /test/ffmpeg_spec.lua: -------------------------------------------------------------------------------- 1 | local ffmpeg = require('ffmpeg') 2 | 3 | describe('ffmpeg', function() 4 | it('should not require Torch', function() 5 | assert.is_nil(_G.torch) 6 | end) 7 | 8 | describe('Video', function() 9 | local video 10 | local n_video_frames = 418 11 | 12 | before_each(function() 13 | video = ffmpeg.new('./test/data/centaur_1.mpg') 14 | end) 15 | 16 | describe(':duration', function() 17 | it('should return video duration in seconds', function() 18 | local expected = 14.0 19 | local actual = video:duration() 20 | assert.is_near(expected, actual, 1.0) 21 | end) 22 | end) 23 | 24 | describe(':pixel_format_name', function() 25 | it('should return pixel format name as a string', function() 26 | local expected = 'yuv420p' 27 | local actual = video:pixel_format_name() 28 | assert.are.same(expected, actual) 29 | end) 30 | end) 31 | 32 | describe(':each_frame', function() 33 | it('should call callback function once per frame', function() 34 | local i = 0 35 | video:each_frame(function() i = i + 1 end) 36 | assert.are.same(n_video_frames, i) 37 | end) 38 | end) 39 | 40 | describe(':read_video_frame', function() 41 | it('should read first frame successfully', function() 42 | local ok = pcall(video.read_video_frame, video) 43 | assert.is_truthy(ok) 44 | end) 45 | 46 | it('should return error after end of stream is reached', function() 47 | local ok = true 48 | for i=0,n_video_frames do 49 | assert.is_truthy(ok) 50 | ok = pcall(video.read_video_frame, video) 51 | end 52 | assert.is_falsy(ok) 53 | end) 54 | end) 55 | 56 | describe(':frame_to_ascii', function() 57 | it('should convert video frame to an ASCII representation', function() 58 | local expected = table.concat({ 59 | ' ', 60 | ' ', 61 | ' . ', 62 | ' ..... ', 63 | ' ...... ', 64 | ' ......- ', 65 | ' .-..... ', 66 | ' ........ ', 67 | ' ........ ', 68 | ' ....... ', 69 | ' ..... ', 70 | ' ... ' 71 | }, '\n') .. '\n' 72 | 73 | local actual = video 74 | :filter('gray', 'scale=40:12') 75 | :read_video_frame() 76 | :to_ascii() 77 | 78 | assert.are.same(expected, actual) 79 | end) 80 | 81 | -- it('ascii video fun', function() 82 | -- local i = 0 83 | -- io.write('\027[H\027[2J') 84 | -- video:filter('gray', 'scale=80:24') 85 | -- video:each_frame(function(frame) 86 | -- io.write(video:frame_to_ascii(frame)) 87 | -- os.execute('sleep 1') 88 | -- i = i + 1 89 | -- end) 90 | -- end) 91 | end) 92 | end) 93 | end) 94 | -------------------------------------------------------------------------------- /test/ffmpeg_torch_spec.lua: -------------------------------------------------------------------------------- 1 | local ffmpeg = require('ffmpeg.torch') 2 | 3 | describe('ffmpeg.torch', function() 4 | describe('Video', function() 5 | local video 6 | local n_video_frames = 418 7 | 8 | before_each(function() 9 | video = ffmpeg.new('./test/data/centaur_1.mpg') 10 | end) 11 | 12 | describe(':to_byte_tensor', function() 13 | it('should convert greyscale frame into a 1-channel byte tensor', function() 14 | -- 3x3 pixel greyscale version of first frame 15 | local expected = {{ 16 | {0, 1, 0}, 17 | {0, 24, 1}, 18 | {0, 18, 0} 19 | }} 20 | 21 | local actual = video 22 | :filter('gray', 'scale=3:3') 23 | :read_video_frame() 24 | :to_byte_tensor() 25 | :totable() 26 | 27 | assert.are.same(expected, actual) 28 | end) 29 | 30 | it('should convert RGB frame into a 3-channel byte tensor', function() 31 | local actual_tensor = video 32 | :filter('rgb24', 'scale=16:16') 33 | :read_video_frame() 34 | :to_byte_tensor() 35 | 36 | assert.are.same(actual_tensor:size():totable(), {3, 16, 16}) 37 | end) 38 | end) 39 | end) 40 | end) 41 | --------------------------------------------------------------------------------