├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── first-timers-only.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── config.lua.example └── nginx-lua-mp4.lua /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug] " 5 | labels: '' 6 | assignees: hom3chuk 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behaviour: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behaviour** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots and Logs** 24 | If applicable, add screenshots and Logs to help explain your problem. 25 | 26 | **Environment** 27 | - nginx or OpenResty and its version 28 | - ffmpeg version 29 | - OS and its version 30 | 31 | **Source video** 32 | If you having an issue with a specific video while other videos are transcoded fine, please provide that video if possible. 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature request]" 5 | labels: '' 6 | assignees: hom3chuk 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/first-timers-only.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: First timers only 3 | about: This is a template for issues that are meant to be contributed by repo (or 4 | OSS in general) first timers. FTO-issue should contain the actual solution diff 5 | in it 6 | title: "[First timers only] " 7 | labels: first-timers-only, good first issue, help wanted, up-for-grabs 8 | assignees: '' 9 | 10 | --- 11 | 12 | [![first-timers-only](https://img.shields.io/badge/first--timers--only-friendly-blue.svg?style=flat-square)](https://www.firsttimersonly.com/) 13 | 14 | This issue is a "First timers only" issue, and it is supposed to be done by someone who has **no previous experience contributing to an open source project**. 15 | 16 | If you are interested in contributing, let us know in the comments below! 17 | 18 | ### What you will need to know 19 | Nothing! This issue is meant to welcome you to open source, and we're happy to walk you through it. 20 | 21 | ### The issue 22 | 23 | 24 | 25 | ## Step by step guide 26 | 27 | - [ ] **Change** the code Update the file [nginx-lua-mp4.lua](https://github.com/Amondo/nginx-lua-mp4/blob/main/nginx-lua-mp4.lua). Press the little pen Icon and edit the lines as shown below. 28 | 29 | Below is a "diff" showing in red (and a `-`) which lines to remove, and in green (and a `+`) which lines to add. 30 | 31 | ```diff 32 | 33 | ``` 34 | 35 | - [ ] **Commit** your changes 36 | 37 | - [ ] **Start a Pull Request.** There are two ways how you can start a pull request: 38 | 39 | - If you are familiar with the terminal or would like to learn it, [here is a great tutorial](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github) on how to send a pull request using the terminal. 40 | 41 | - You can also [edit files directly in your browser](https://help.github.com/articles/editing-files-in-your-repository/) and open a pull request from there. 42 | 43 | - [ ] **Done!** Ask in comments for a review 🤓 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | .idea 4 | config.lua 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | developer@amondo.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to nginx-lua-mp4 2 | 3 | ## Code of Conduct 4 | 5 | We have [Code of Conduct](https://github.com/Amondo/nginx-lua-mp4/blob/main/CODE_OF_CONDUCT.md) and all contributors are expected to uphold it. 6 | 7 | ## Reporting bugs 8 | 9 | If you run into any bugs, please create a [GitHub Issue](https://github.com/Amondo/nginx-lua-mp4/issues). nginx-lua-mp4 relies heavily on the third party software, so be sure to include detailed reproduction steps and as much info about your environment as possible. 10 | 11 | Try to include as much as possible from this list: 12 | - A brief summary of the issue 13 | - nginx/OpenResty version 14 | - ffmpeg version 15 | - OS 16 | - Steps to reproduce the issue 17 | - What is the expected behaviour 18 | - What actually happens 19 | - Error messages/logs 20 | 21 | ## Contributing code 22 | ### Github flow 23 | 24 | We use [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow). In order to contribute code: 25 | 1. Create a fork 26 | 2. In that fork, create a feature branch 27 | 3. Commit changes to that branch 28 | 4. Create a Pull Request to the `nginx-lua-mp4` repo (mark "Work in Progress" PRs with `[WIP]` in the title) 29 | 30 | ## License 31 | 32 | By contributing, you agree that your contribution will be licensed under MIT license 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Amondo 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 | Navigation: 2 | * [Features](#features) 3 | * [Requirements](#requirements) 4 | * [Installation](#installation) 5 | * [Update](#update) 6 | * [Flags](#flags) 7 | 8 | # nginx-lua-mp4 9 | 10 | nginx-lua-mp4 (or simply _luamp_) is a LUA module for [OpenResty](https://openresty.org/en/) or nginx with [ngx_http_lua_module](https://github.com/openresty/lua-nginx-module) that allows on-the-fly video transcoding using [ffmpeg](https://ffmpeg.org/) controlled by parameters passed in the URL. 11 | 12 | To put it simply, if you have a 1920x1080 video file 13 | `https://example.com/vids/cat.mp4`, you can request URL `https://example.com/vids/w_640/cat.mp4` and you will get a 640x360 of the same video. You can crop/scale, keep aspect ratio or discard it, add padding around video (need a dynamic blurred padding? We got you covered). 14 | 15 | https://user-images.githubusercontent.com/3368441/161866581-ee1c745c-f119-430c-810c-f13a820a6c4b.mp4 16 | 17 | ## Features 18 | 19 | ✅ — ready, 🚧 — WIP 20 | 21 | - General: 22 | - ✅ Can (optionally) download original videos from the upstream, if missing from the local file system 23 | - ✅ Configurable transcoding flags in URL, can be dropped in place of existing system without front end rewrite 24 | - ✅ Configurable logging for debug/setup 25 | - ✅ [DPR (device pixel ratio) support](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio) 26 | - ✅ Pass data back to nginx location after transcoding, so you can customize post-process or call more LUA modules 27 | - ✅ Serve original on failed transcoding 28 | - Transcoding: 29 | - ✅ Scale/Resize 30 | - Padding: 31 | - ✅ Black box padding 32 | - ✅ Blurred background padding 33 | - ✅ Upscale protection 34 | - 🚧 Coloured box padding 35 | - ✅ Keep original aspect ratio 36 | - ✅ mp4 support (output) 37 | - 🚧 webm support (output) 38 | ## Requirements 39 | 40 | - OpenResty or nginx with ngx_http_lua_module enabled 41 | - [ffmpeg 5](https://launchpad.net/~savoury1/+archive/ubuntu/ffmpeg5) installed 42 | - [time](https://en.wikipedia.org/wiki/Time_(Unix)) utility if you have `config.logTime` enabled 43 | 44 | ## Installation 45 | 46 | #### 1. Clone the repo 47 | 48 | #### 2. nginx config changes 49 | 50 | Add to the `http` section of the nginx config: 51 | 52 | ``` 53 | http { 54 | ... 55 | 56 | lua_package_path "/absolute/path/to/nginx-lua-mp4/?.lua;;"; 57 | 58 | ... 59 | ``` 60 | 61 | And here's minimal viable config for 4 locations you need to set up. These locations are described in the sections below: 62 | ``` 63 | # video location 64 | location ~ ^/(?([0-9a-zA-Z_,\.:]+)\/|)(?[0-9a-zA-Z_\-\.]+\.mp4)$ { 65 | # these two are required to be set regardless 66 | set $luamp_original_video ""; 67 | set $luamp_transcoded_video ""; 68 | 69 | # these are needed to be set if you did not use them in regex matching location 70 | set $luamp_prefix ""; 71 | set $luamp_postfix ""; 72 | 73 | #pass to transcoder location 74 | try_files $uri @luamp_process; 75 | } 76 | 77 | # process/transcode location 78 | location @luamp_process { 79 | content_by_lua_file "/absolute/path/to/nginx-lua-mp4/nginx-lua-mp4.lua"; 80 | } 81 | 82 | # cache location 83 | location =/luamp-cache { 84 | internal; 85 | root /; 86 | index off; 87 | 88 | set_unescape_uri $luamp_transcoded_video $arg_luamp_cached_video_path; 89 | 90 | try_files $luamp_transcoded_video =404; 91 | } 92 | 93 | # upstream location 94 | location =/luamp-upstream { 95 | internal; 96 | rewrite ^(.+)$ $luamp_original_video break; 97 | proxy_pass https://old-cdn.example.com; 98 | } 99 | 100 | ``` 101 | 102 | #### 2.1. Video location 103 | This location used as an entry point and to set initial variables. This is usually a location with a `.mp4` at the end. 104 | 105 | There are two variables you need to `set`/initialise: `$luamp_original_video` and `$luamp_transcoded_video`. 106 | 107 | There are four variables that may be used as a named capture group in location regex: `luamp_prefix`, `luamp_flags`, `luamp_postfix`, `luamp_filename`. 108 | 109 | For example: 110 | 111 | ``` 112 | https://example.com/asset/video/width_1980,height_1080,crop_padding/2019/12/new_year_boardgames_party.mp4 113 | luamp_prefix: asset/video/ 114 | luamp_flags: width_1980,height_1080,crop_padding 115 | luamp_postfix: 2019/12/ 116 | luamp_filename: new_year_boardgames_party.mp4 117 | ``` 118 | 119 | If you do not need prefix and postfix, you can omit them from the regexp, but do make sure you `set` them to an empty string in the location. Here's the minimal viable example for simpler URLs with no prefix/postfix: 120 | 121 | ``` 122 | location ~ ^/(?([0-9a-zA-Z_,\.:]+)\/|)(?[0-9a-zA-Z_\-\.]+\.mp4)$ { 123 | # these two are required to be set regardless 124 | set $luamp_original_video ""; 125 | set $luamp_transcoded_video ""; 126 | 127 | # these are needed to be set if you did not use them in regex matching location 128 | set $luamp_prefix ""; 129 | set $luamp_postfix ""; 130 | 131 | #pass to transcoder location 132 | try_files $uri @luamp_process; 133 | } 134 | 135 | ``` 136 | 137 | #### Security considerations 138 | `prefix`, `postfix` and `filename` are passed to the `os.execute()` with following sanitisation: 139 | - alphanumeric symbols, underscores, dots and slashes are allowed. 140 | - all other symbols are stripped. 141 | - then, double dots are stripped. 142 | 143 | These sanitisation rules are enough to prevent shell injections and path traversal. However, especially if you decide to modify luamp code, consider using following capture patterns in the video location: 144 | - `(?[0-9a-zA-Z_\-\.\/]+\/)` 145 | - `(?([0-9a-zA-Z_,\.:]+)\/|)` 146 | - `(?[0-9a-zA-Z_\-\.\/]+\/)` 147 | - `(?[0-9a-zA-Z_\-\.]+\.mp4)` 148 | 149 | #### 2.2. Process location 150 | 151 | Process location is pretty simple, it just passes execution to the LUA part of luamp module: 152 | 153 | ``` 154 | location @luamp_process { 155 | content_by_lua_file "/absolute/path/to/nginx-lua-mp4/nginx-lua-mp4.lua"; 156 | } 157 | ``` 158 | 159 | #### 2.3. Cache location 160 | 161 | Cache location is where previously transcoded videos are served from: 162 | 163 | ``` 164 | location =/luamp-cache { 165 | internal; 166 | root /; 167 | index off; 168 | 169 | set_unescape_uri $luamp_transcoded_video $arg_luamp_cached_video_path; 170 | 171 | try_files $luamp_transcoded_video =404; 172 | } 173 | ``` 174 | 175 | #### 2.4. Upstream location 176 | 177 | Upstream location is used when you have no original video files stored on your local file system (perhaps, those are stored on your old CDN, or elsewhere). 178 | If `luamp` finds no original file to transcode, it will attempt to download it from the upstream specified: 179 | ``` 180 | location =/luamp-upstream { 181 | internal; 182 | rewrite ^(.+)$ $luamp_original_video break; 183 | proxy_pass https://old-cdn.example.com; 184 | } 185 | ``` 186 | 187 | `$luamp_original_video` is set within `config.getOriginalsUpstreamPath` function that can be configured in `luamp` config.lua. You can apply whatever logic you may need there to dynamically generate path for the upstream. 188 | 189 | #### 3. nginx-lua-mp4 config 190 | 191 | Go to the directory you downloaded `luamp` to: 192 | 193 | ``` 194 | $ cd /absolute/path/to/nginx-lua-mp4/ 195 | ``` 196 | 197 | Create a config file by copying the example config: 198 | 199 | ``` 200 | $ cp config.lua.example config.lua 201 | ``` 202 | 203 | Open it with a text editor of your choice and change the variables you feel like changing. 204 | 205 | ``` 206 | $ nano config.lua 207 | ``` 208 | 209 | #### `config.downloadOriginals` 210 | 211 | When set to `true`, `luamp` will attempt to download missing original videos from the upstream. Set it to `false` if you have original videos provided by other means to this directory: 212 | ``` 213 | config.mediaBaseFilepath/$prefix/$postfix/$filename 214 | ``` 215 | 216 | #### `config.ffmpeg` 217 | 218 | Path to the `ffmpeg` executable. Can be figured out by using `which` command in the terminal: 219 | 220 | ``` 221 | $ which ffmpeg 222 | /usr/local/bin/ffmpeg 223 | ``` 224 | 225 | #### `config.ffmpegDevNull` 226 | 227 | Where to redirect `ffmpeg` output if `config.logFfmpegOutput` is set to false. 228 | 229 | For *nix (default value): 230 | ``` 231 | config.ffmpegDevNull = '2> /dev/null' -- nix 232 | ``` 233 | 234 | For win: 235 | ``` 236 | config.ffmpegDevNull = '2>NUL' -- win 237 | ``` 238 | 239 | #### `config.flagMap` 240 | 241 | Use this table to customize how flags are called in your URLs. Defaults are one letter flags like `w` for `width`, but you can customise these by editing left side of `flagMap` table: 242 | 243 | One letter flags (except for DPR) if you want to use flags like `w_200,h_180,c_pad`: 244 | 245 | ``` 246 | ['c'] = 'crop', 247 | ['b'] = 'background', 248 | ['dpr'] = 'dpr', 249 | ['h'] = 'height', 250 | ['w'] = 'width', 251 | ``` 252 | 253 | Full flags if you want to use flags like `width_200,height_180,crop_pad`: 254 | 255 | ``` 256 | ['crop'] = 'crop', 257 | ['background'] = 'background', 258 | ['dpr'] = 'dpr', 259 | ['height'] = 'height', 260 | ['width'] = 'width', 261 | ``` 262 | 263 | #### `config.flagPreprocessHook(flag, value)` 264 | 265 | Customize this function to preprocess flags or their values. Return values should contain values that are present in `config.flagMap` and `config.flagValueMap`. 266 | 267 | #### `config.flagsDelimiter` 268 | 269 | Character that is used to separate different flags in URL, e.g. commas in `/w_1280,h_960,c_pad/`. 270 | 271 | #### `config.flagValueDelimiter` 272 | 273 | Character that is used to separate flag name from the value, e.g. underscores in `/w_1280,h_960,c_pad/`. 274 | 275 | #### `config.flagValueMap` 276 | 277 | Similar to `config.flagMap` above, but for non-number flag *values* rather than flag names. 278 | 279 | Default flag values, e.g. `c_pad`, `c_lpad` or `c_fill`, also `b_blurred`: 280 | 281 | ``` 282 | config.flagValueMap = { 283 | ['pad'] = 'padding', 284 | ['lpad'] = 'limited_padding', 285 | ['fill'] = 'fill', 286 | ['blurred'] = 'blur', 287 | } 288 | ``` 289 | 290 | Full flag values, e.g. `c_padding` or `c_limited-padding`: 291 | ``` 292 | config.flagValueMap = { 293 | ['pading'] = 'padding', 294 | ['limited-padding'] = 'limited_padding', 295 | ['blurred'] = 'blur', 296 | } 297 | ``` 298 | 299 | #### `config.getOriginalsUpstreamPath` 300 | 301 | Function that is used to generate a URL for upstream. `prefix`, `postfix` and `filename` are provided to the function and you can also use [LUA ngx API](https://openresty-reference.readthedocs.io/en/latest/Lua_Nginx_API/). 302 | 303 | #### `config.logEnabled = true` 304 | 305 | Whether to log whole `luamp` process. Useful for initial setup and for debug. 306 | 307 | #### `config.logFfmpegOutput` 308 | 309 | Whether to log `ffmpeg` output. Note that `ffmpeg` outputs to `stderr`, and if `logFfmpegOutput` is enabled, it will log to nginx's `error.log`. 310 | 311 | #### `config.logLevel = ngx.ERR` 312 | 313 | Log level, available values: `ngx.STDERR`, `ngx.EMERG`, `ngx.ALERT`, `ngx.CRIT`, `ngx.ERR`, `ngx.WARN`, `ngx.NOTICE`, `ngx.INFO`, `ngx.DEBUG`. 314 | 315 | #### `config.logTime` 316 | 317 | Whether to prepend `ffmpeg` command with `time` utility, if you wish to log time spent in transcoding. 318 | 319 | #### `config.maxHeight` and `config.maxWidth` 320 | 321 | Limit the output video's maximum height or width. If the resulting height or width is exceeding the limit (for example, after a high DPR calculation), it will be capped at the `config.maxHeight` and `config.maxWidth`. 322 | 323 | #### `config.mediaBaseFilepath` 324 | 325 | Where videos (both originals and transcoded ones) should be stored. Usually, a directory where assets are stored. Should be readable/writable for nginx. 326 | 327 | #### `config.minimumTranscodedFileSize` 328 | 329 | Minimum required size (in bytes) for the transcoded file to not be considered broken and deleted (default is 1KB). 330 | 331 | During the transcoding, errors may occur and ffmpeg sometimes leaves corrupt files on the FS. Those are usually either 0B or just a few bytes of header. Luamp will delete those that are less than `minimumTranscodedFileSize` bytes. 332 | 333 | #### `config.serveOriginalOnTranscodeFailure` 334 | 335 | Serve original file when transcode failed. If set to `false`, luamp will respond with 404 in this case 336 | 337 | ## Update 338 | 339 | To update luamp, just do a `git pull`. 340 | 341 | After the pull, there may be some additions to the config file. Now, one can use a `diff` tool, but it will spew out all the differences, which can be overwhelming when you customize a lot in your config as compared to what is in the example config: 342 | 343 | ``` 344 | nginx-lua-mp4 $ diff config.lua config.lua.example 345 | 52c52 346 | < config.logEnabled = true 347 | --- 348 | > config.logEnabled = false 349 | 59c59 350 | < config.logFfmpegOutput = true 351 | --- 352 | > config.logFfmpegOutput = false 353 | 66c66,72 354 | < config.logTime = true 355 | --- 356 | > config.logTime = false 357 | > 358 | > -- top limit for output video height (default 4k UHD) 359 | > config.maxHeight = 2160 360 | > 361 | > -- top limit for output video width (default 4k UHD) 362 | > config.maxWidth = 3840 363 | 364 | ``` 365 | 366 | You definitely want to keep what was customized by you but also to get new config options that may be necessary. This one-liner will show you all the __new__ config entries, omitting what was customised, given you did not move options around and just edited them in place: 367 | 368 | ``` 369 | nginx-lua-mp4 $ sdiff -s config.lua config.lua.example | grep -e '\s*>' | sed -ne "s/^[[:space:]]*>\t//p" 370 | -- top limit for output video height (default 4k UHD) 371 | config.maxHeight = 2160 372 | -- top limit for output video width (default 4k UHD) 373 | config.maxWidth = 3840 374 | ``` 375 | 376 | You can now just copy and paste these lines above the `return config` in `config.lua` and that's it 👌 377 | 378 | ## Flags 379 | 380 | ### `b` — Background 381 | 382 | Available values: 383 | - `blurred` — when padding is enabled (with `c_pad` or `c_lpad`), the padding box will contain an upscaled blurred video. 384 | 385 | ### `c` — Crop 386 | 387 | Available values: 388 | - `pad` — when resizing a video, aspect ratio will be preserved and padding box will be added to keep the aspect ratio. 389 | - `lpad` — same as `pad` but original video will **not** be scaled up. 390 | - `fill` — scales and crops a video to fit specified dimensions without distortion, scaling it to fill the space and cropping any excess. 391 | 392 | ### `dpr` — Device Pixel Ratio 393 | 394 | Available values: float or integer number. 395 | 396 | ### `h` — Height 397 | 398 | Available values: integer number. 399 | 400 | ### `w` — Width 401 | 402 | Available values: integer number. 403 | 404 | ### `x` — X coordinate for overlay with `[limited_]padding` crop 405 | 406 | Available values: 407 | - integer number for pixels 408 | - decimal number in range `(0, 1)` for percentage: 0.25 is 25% of resulting width (after DPR is applied) 409 | 410 | ### `y` — Y coordinate for overlay with `[limited_]padding` crop 411 | 412 | Available values: 413 | - integer number for pixels 414 | - decimal number in range `(0, 1)` for percentage: 0.25 is 25% of resulting height (after DPR is applied) 415 | -------------------------------------------------------------------------------- /config.lua.example: -------------------------------------------------------------------------------- 1 | config = {} 2 | 3 | -- ########## CONFIG ########## 4 | -- change according to your needs 5 | 6 | -- `which ffmpeg` 7 | config.ffmpeg = '/usr/local/bin/ffmpeg' 8 | 9 | -- where to save original and transcoded files (trailing slash required) 10 | config.mediaBaseFilepath = '/tmp/nginx/' 11 | 12 | -- set to `true` to enable originals download from the upstream/CDN. See `getOriginalsUpstreamUrl` below 13 | config.downloadOriginals = true 14 | 15 | -- function to get a URL where originals are stored, when `downloadOriginals` set to true. 16 | function config.getOriginalsUpstreamPath(prefix, postfix, filename) 17 | -- return ngx.var.request_uri 18 | return (prefix or '') .. (postfix or '') .. (filename or '') 19 | end 20 | 21 | -- character that is used to separate different flags in URL. 22 | -- eg commas in `/w_1280,h_960,c_pad/` 23 | config.flagsDelimiter = ',' 24 | 25 | -- character that is used to separate flag name from the value. 26 | -- eg underscores in `/w_1280,h_960,c_pad/` 27 | config.flagValueDelimiter = '_' 28 | 29 | -- override URL flag names. Useful when you migrate from another transcoding solution and already have 30 | -- some flags in use on the front end. Customize the left part of the table. 31 | -- eg `['cropping'] = 'crop'` to use `cropping` instead of the default `c` 32 | config.flagMap = { 33 | ['c'] = 'crop', -- crop / scale 34 | ['b'] = 'background', 35 | ['dpr'] = 'dpr', -- DPR, https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio 36 | -- ['f'] = 'format', 37 | ['h'] = 'height', 38 | ['w'] = 'width', 39 | ['x'] = 'x', 40 | ['y'] = 'y', 41 | } 42 | 43 | -- override URL flag values. Useful when you migrate from another transcoding solution and already have 44 | -- some flag values in use on the front end. Customize the left part of the table 45 | -- eg `['padded'] = 'pad'` to use `padded` instead of the default `pad` 46 | -- Also, all flag values not present in this table will be considered (and cast to) a number 47 | config.flagValueMap = { 48 | ['pad'] = 'padding', 49 | ['lpad'] = 'limited_padding', 50 | ['fill'] = 'fill', 51 | ['blurred'] = 'blur', 52 | } 53 | 54 | -- log transcoding process. Useful when doing initial setup or debugging issues 55 | config.logEnabled = false 56 | config.logLevel = ngx.ERR 57 | -- config.logLevel = ngx.INFO 58 | -- config.logLevel = ngx.DEBUG 59 | 60 | -- log `ffmpeg` output. 61 | -- If enabled, will log ffmpeg transcoding process output to error log (because that's where ffmpeg outputs to) 62 | config.logFfmpegOutput = false 63 | 64 | -- where to redirect ffmpeg output to (if `logFfmpegOutput = false`) 65 | config.ffmpegDevNull = '2> /dev/null' -- nix 66 | -- config.ffmpegDevNull = '2>NUL' -- win 67 | 68 | -- whether to prepend transcoding command with `time` utility to log time spent in ffmpeg 69 | config.logTime = false 70 | 71 | -- top limit for output video height (default 4k UHD) 72 | config.maxHeight = 2160 73 | 74 | -- top limit for output video width (default 4k UHD) 75 | config.maxWidth = 3840 76 | 77 | -- customize this function to preprocess flags or their values 78 | -- return values should contain values that are present in `config.flagMap` and `config.flagValueMap` 79 | function config.flagPreprocessHook(flag, value) 80 | -- do some processing 81 | -- strip sub parameters `c_pad:pink` -> `c_pad` 82 | -- value = value:gsub('([^:]+):?.*', '%1') 83 | return flag, value 84 | end 85 | 86 | -- serve original file when transcode failed. 87 | config.serveOriginalOnTranscodeFailure = true 88 | 89 | -- least required size (in bytes) for the transcoded file to not be considered broken and deleted (default is 1KB) 90 | config.minimumTranscodedFileSize = 1024 91 | 92 | -- encoding preset to use https://trac.ffmpeg.org/wiki/Encode/H.264 93 | config.ffmpegPreset = '' 94 | -- config.ffmpegPreset = 'ultrafast' 95 | -- config.ffmpegPreset = 'superfast' 96 | -- config.ffmpegPreset = 'veryfast' 97 | 98 | return config 99 | -------------------------------------------------------------------------------- /nginx-lua-mp4.lua: -------------------------------------------------------------------------------- 1 | config = require('config') 2 | 3 | function log(data) 4 | if (config.logEnabled == true) then 5 | ngx.log(config.logLevel, data) 6 | end 7 | end 8 | 9 | function setDefaultConfig(option, value) 10 | if config[option] == nil then 11 | log('setting default config for ' .. option) 12 | config[option] = value 13 | end 14 | end 15 | 16 | function cleanupPath(path) 17 | -- allow only alphanumeric + underscore + dash + slash + dot 18 | local retVal = path:gsub('[^%w_%-/.=]', '') 19 | -- strip double+ dot 20 | return retVal:gsub('([\\.][\\.]+)', '') 21 | end 22 | 23 | local configDefaults = { 24 | ['minimumTranscodedFileSize'] = 1024, 25 | ['serveOriginalOnTranscodeFailure'] = true, 26 | ['ffmpegPreset'] = '', 27 | } 28 | 29 | log('luamp started') 30 | 31 | -- set missing config options to the defaults 32 | for o, v in pairs(configDefaults) do 33 | setDefaultConfig(o, v) 34 | end 35 | 36 | -- get url params 37 | local prefix, flags, postfix, filename = ngx.var.luamp_prefix, ngx.var.luamp_flags, ngx.var.luamp_postfix, ngx.var.luamp_filename 38 | 39 | log('prefix: ' .. prefix) 40 | log('flags: ' .. flags) 41 | log('postfix: ' .. postfix) 42 | log('filename: ' .. filename) 43 | 44 | prefix = cleanupPath(prefix) 45 | postfix = cleanupPath(postfix) 46 | filename = cleanupPath(filename) 47 | 48 | local flagValues = {} 49 | local flagOrdered = {} 50 | local enabledFlags = { 51 | ['crop'] = true, 52 | ['background'] = true, 53 | ['dpr'] = true, 54 | -- ['format'] = true, 55 | ['height'] = true, 56 | ['width'] = true, 57 | ['x'] = true, 58 | ['y'] = true, 59 | } 60 | 61 | -- parse flags into table 62 | for flag, value in string.gmatch(flags, '(%w+)' .. config.flagValueDelimiter .. '([^' .. config.flagsDelimiter .. '\\/]+)' .. config.flagsDelimiter .. '*') do 63 | -- if the flag is enabled 64 | if value ~= nil and enabledFlags[config.flagMap[flag]] ~= nil then 65 | if config.flagPreprocessHook ~= nil then 66 | flag, value = config.flagPreprocessHook(flag, value) 67 | end 68 | log(config.flagMap[flag] .. ' ' .. value) 69 | -- add it 70 | table.insert(flagOrdered, config.flagMap[flag]) 71 | -- if it is an allowed text flag 72 | if config.flagValueMap[value] ~= nil then 73 | -- add allowed text flag 74 | flagValues[config.flagMap[flag]] = config.flagValueMap[value] 75 | else 76 | -- otherwise cast to number 77 | flagValues[config.flagMap[flag]] = tonumber(value) 78 | end 79 | end 80 | end 81 | 82 | -- sort flags so path will be the same for `w_1280,h_960` and `h_960,w_1280` 83 | table.sort(flagOrdered) 84 | 85 | function coalesceFlag(option) 86 | if flagValues[option] ~= nil then 87 | return option .. '_' .. flagValues[option] 88 | else 89 | return '' 90 | end 91 | end 92 | 93 | -- make path 94 | local options = {} 95 | local optionsPath = '' 96 | 97 | for _, option in ipairs(flagOrdered) do 98 | table.insert(options, coalesceFlag(option)) 99 | end 100 | 101 | optionsPath = table.concat(options, '/') 102 | if optionsPath ~= '' then 103 | optionsPath = optionsPath .. '/' 104 | end 105 | 106 | -- check if we already have cached version of a file 107 | local cachedFilepath = config.mediaBaseFilepath .. (prefix or '') .. (optionsPath or '') .. (postfix or '') 108 | local originalFilepath = config.mediaBaseFilepath .. (prefix or '') .. (postfix or '') 109 | log('checking for cached transcoded version at: ' .. cachedFilepath .. filename) 110 | local cachedFile = io.open(cachedFilepath .. filename, 'r') 111 | 112 | if cachedFile == nil then 113 | log('no cached file') 114 | -- create cached version 115 | 116 | -- check if we have original file to transcode 117 | log('checking for original version at: ' .. originalFilepath .. filename) 118 | local originalFileCheck = io.open(originalFilepath .. filename) 119 | 120 | -- check if we have original 121 | if not originalFileCheck then 122 | log('no original') 123 | if config.downloadOriginals then 124 | -- download original, if upstream download is enabled 125 | log('downloading original from ' .. config.getOriginalsUpstreamPath(prefix, postfix, filename)) 126 | -- clear body 127 | ngx.req.discard_body() 128 | log('fetching') 129 | -- fetch 130 | local originalReq = ngx.location.capture('/luamp-upstream', { vars = { luamp_original_video = config.getOriginalsUpstreamPath(prefix, postfix, filename) } }) 131 | log('upstream status: ' .. originalReq.status) 132 | if originalReq.status == ngx.HTTP_OK and originalReq.body:len() > 0 then 133 | log('downloaded original, saving') 134 | os.execute('mkdir -p ' .. originalFilepath) 135 | local originalFile = io.open(originalFilepath .. filename, 'w') 136 | originalFile:write(originalReq.body) 137 | originalFile:close() 138 | log('saved to ' .. originalFilepath .. filename) 139 | else 140 | ngx.exit(ngx.HTTP_NOT_FOUND) 141 | end 142 | else 143 | ngx.exit(ngx.HTTP_NOT_FOUND) 144 | end 145 | else 146 | log('original is present on local FS') 147 | originalFileCheck:close() 148 | end 149 | 150 | -- process DPR 151 | if (flagValues['dpr'] ~= nil) then 152 | log('before DPR calculation, w: ' .. (flagValues['width'] or 'nil') .. ', h: ' .. (flagValues['height'] or 'nil') .. ', x: ' .. (flagValues['x'] or 'nil') .. ', y: ' .. (flagValues['y'] or 'nil')) 153 | -- width and height 154 | if flagValues['height'] ~= nil then 155 | flagValues['height'] = math.ceil(flagValues['height'] * flagValues['dpr']) 156 | end 157 | if flagValues['width'] ~= nil then 158 | flagValues['width'] = math.ceil(flagValues['width'] * flagValues['dpr']) 159 | end 160 | 161 | -- x and y 162 | if flagValues['x'] ~= nil and flagValues['x'] >= 1 then 163 | flagValues['x'] = flagValues['x'] * flagValues['dpr'] 164 | end 165 | if flagValues['y'] ~= nil and flagValues['y'] >= 1 then 166 | flagValues['y'] = flagValues['y'] * flagValues['dpr'] 167 | end 168 | log('after DPR calculation, w: ' .. (flagValues['width'] or 'nil') .. ', h: ' .. (flagValues['height'] or 'nil') .. ', x: ' .. (flagValues['x'] or 'nil') .. ', y: ' .. (flagValues['y'] or 'nil')) 169 | end 170 | 171 | if config.maxHeight ~= nil and flagValues['height'] ~= nil then 172 | if flagValues['height'] > config.maxHeight then 173 | log('resulting height exceeds configured limit, capping it at ' .. config.maxHeight) 174 | flagValues['height'] = config.maxHeight 175 | end 176 | end 177 | 178 | if config.maxWidth ~= nil and flagValues['width'] ~= nil then 179 | if flagValues['width'] > config.maxWidth then 180 | log('resulting width exceeds configured limit, capping it at ' .. config.maxWidth) 181 | flagValues['width'] = config.maxWidth 182 | end 183 | end 184 | 185 | -- calculate absolute x/y for values in (0, 1) range 186 | if flagValues['x'] ~= nil and flagValues['x'] > 0 and flagValues['x'] < 1 then 187 | flagValues['x'] = flagValues['x'] * flagValues['width'] 188 | log('absolute x: ' .. flagValues['x']) 189 | end 190 | if flagValues['y'] ~= nil and flagValues['y'] > 0 and flagValues['y'] < 1 then 191 | flagValues['y'] = flagValues['y'] * flagValues['height'] 192 | log('absolute y: ' .. flagValues['y']) 193 | end 194 | 195 | local preset = '' 196 | -- setting x264 preset 197 | if (config['ffmpegPreset'] ~= '') then 198 | log('x264 preset: ' .. config['ffmpegPreset']) 199 | preset = ' -preset ' .. config['ffmpegPreset'] .. ' ' 200 | end 201 | 202 | log('transcoding to ' .. cachedFilepath .. filename) 203 | 204 | -- create cached transcoded file 205 | os.execute('mkdir -p ' .. cachedFilepath) 206 | 207 | -- create command 208 | local command 209 | 210 | if (flagValues['background'] ~= nil and flagValues['background'] == 'blur' and flagValues['crop'] ~= nil and flagValues['crop'] == 'limited_padding' and flagValues['width'] ~= nil and flagValues['height'] ~= nil) then 211 | -- scale + padded (no upscale) + blurred bg 212 | command = config.ffmpeg .. ' -i ' .. originalFilepath .. filename .. ' -filter_complex "split [first][second];[first]hue=b=-1,boxblur=20, scale=max(' .. flagValues['width'] .. '\\,iw*(max(' .. flagValues['width'] .. '/iw\\,' .. flagValues['height'] .. '/ih))):max(' .. flagValues['height'] .. '\\,ih*(max(' .. flagValues['width'] .. '/iw\\,' .. flagValues['height'] .. '/ih))):force_original_aspect_ratio=increase:force_divisible_by=2, crop=' .. flagValues['width'] .. ':' .. flagValues['height'] .. ', setsar=1[background];[second]scale=min(' .. flagValues['width'] .. '\\,iw):min(' .. flagValues['height'] .. '\\,ih):force_original_aspect_ratio=decrease:force_divisible_by=2,setsar=1[foreground];[background][foreground]overlay=y=' .. (flagValues['y'] or '(H-h)/2') .. ':x=' .. (flagValues['x'] or '(W-w)/2') .. '" -c:a copy ' .. preset .. cachedFilepath .. filename 213 | 214 | elseif (flagValues['background'] ~= nil and flagValues['background'] == 'blur' and flagValues['crop'] ~= nil and flagValues['crop'] == 'padding' and flagValues['width'] ~= nil and flagValues['height'] ~= nil) then 215 | -- scale + padded (with upscale) + blurred bg 216 | command = config.ffmpeg .. ' -i ' .. originalFilepath .. filename .. ' -filter_complex "split [first][second];[first]hue=b=-1,boxblur=20, scale=max(' .. flagValues['width'] .. '\\,iw*(max(' .. flagValues['width'] .. '/iw\\,' .. flagValues['height'] .. '/ih))):max(' .. flagValues['height'] .. '\\,ih*(max(' .. flagValues['width'] .. '/iw\\,' .. flagValues['height'] .. '/ih))):force_original_aspect_ratio=increase:force_divisible_by=2, crop=' .. flagValues['width'] .. ':' .. flagValues['height'] .. ', setsar=1[background];[second]scale=min(' .. flagValues['width'] .. '\\,iw*(min(' .. flagValues['width'] .. '/iw\\,' .. flagValues['height'] .. '/ih))):min(' .. flagValues['height'] .. '\\,ih*(min(' .. flagValues['width'] .. '/iw\\,' .. flagValues['height'] .. '/ih))):force_original_aspect_ratio=increase:force_divisible_by=2,setsar=1[foreground];[background][foreground]overlay=y=' .. (flagValues['y'] or '(H-h)/2') .. ':x=' .. (flagValues['x'] or '(W-w)/2') .. '" -c:a copy ' .. preset .. cachedFilepath .. filename 217 | 218 | elseif (flagValues['crop'] ~= nil and flagValues['crop'] == 'limited_padding' and flagValues['width'] ~= nil and flagValues['height'] ~= nil) then 219 | -- scale (no upscale) with padding (blackbox) 220 | command = config.ffmpeg .. ' -i ' .. originalFilepath .. filename .. ' -filter_complex "scale=min(' .. flagValues['width'] .. '\\,iw):min(' .. flagValues['height'] .. '\\,ih):force_original_aspect_ratio=decrease:force_divisible_by=2,setsar=1,pad=' .. flagValues['width'] .. ':' .. flagValues['height'] .. ':y=' .. (flagValues['y'] or '-1') .. ':x=' .. (flagValues['x'] or '-1') .. ':color=black" -c:a copy ' .. preset .. cachedFilepath .. filename 221 | 222 | elseif (flagValues['crop'] ~= nil and flagValues['crop'] == 'padding' and flagValues['width'] ~= nil and flagValues['height'] ~= nil) then 223 | -- scale (with upscale) with padding (blackbox) 224 | command = config.ffmpeg .. ' -i ' .. originalFilepath .. filename .. ' -filter_complex "scale=min(' .. flagValues['width'] .. '\\,iw*(min(' .. flagValues['width'] .. '/iw\\,' .. flagValues['height'] .. '/ih))):min(' .. flagValues['height'] .. '\\,ih*(min(' .. flagValues['width'] .. '/iw\\,' .. flagValues['height'] .. '/ih))):force_original_aspect_ratio=increase:force_divisible_by=2,setsar=1,pad=' .. flagValues['width'] .. ':' .. flagValues['height'] .. ':y=' .. (flagValues['y'] or '-1') .. ':x=' .. (flagValues['x'] or '-1') .. ':color=black" -c:a copy ' .. preset .. cachedFilepath .. filename 225 | 226 | elseif (flagValues['crop'] ~= nil and flagValues['crop'] == 'fill' and flagValues['width'] ~= nil and flagValues['height'] ~= nil) then 227 | -- scale (with upscale) + cropping 228 | command = config.ffmpeg .. ' -i ' .. originalFilepath .. filename .. ' -filter_complex "scale=max(' .. flagValues['width'] .. '\\,iw*(max(' .. flagValues['width'] .. '/iw\\,' .. flagValues['height'] .. '/ih))):max(' .. flagValues['height'] .. '\\,ih*(max(' .. flagValues['width'] .. '/iw\\,' .. flagValues['height'] .. '/ih))):force_original_aspect_ratio=increase:force_divisible_by=2,crop=' .. flagValues['width'] .. ':' .. flagValues['height'] .. ',setsar=1" -c:a copy ' .. preset .. cachedFilepath .. filename 229 | 230 | elseif (flagValues['width'] ~= nil and flagValues['height'] ~= nil) then 231 | -- simple scale (no aspect ratio) 232 | command = config.ffmpeg .. ' -i ' .. originalFilepath .. filename .. ' -filter_complex "scale=' .. flagValues['width'] .. ':' .. flagValues['height'] .. ':force_divisible_by=2:force_original_aspect_ratio=disable,setsar=1" -c:a copy ' .. preset .. cachedFilepath .. filename 233 | 234 | elseif (flagValues['height'] ~= nil) then 235 | -- simple one-side scale (h) 236 | command = config.ffmpeg .. ' -i ' .. originalFilepath .. filename .. ' -filter_complex "scale=-1:' .. flagValues['height'] .. ':force_divisible_by=2:force_original_aspect_ratio=decrease,setsar=1" -c:a copy ' .. preset .. cachedFilepath .. filename 237 | 238 | elseif (flagValues['width'] ~= nil) then 239 | -- simple one-side scale (w) 240 | command = config.ffmpeg .. ' -i ' .. originalFilepath .. filename .. ' -filter_complex "scale=' .. flagValues['width'] .. ':-1:force_divisible_by=2:force_original_aspect_ratio=decrease,setsar=1" -c:a copy ' .. preset .. cachedFilepath .. filename 241 | 242 | end 243 | 244 | local executeSuccess 245 | 246 | if command ~= nil then 247 | if config.logFfmpegOutput == false then 248 | command = command .. ' ' .. config.ffmpegDevNull 249 | end 250 | if config.logTime then 251 | command = 'time ' .. command 252 | end 253 | log('ffmpeg command: ' .. command) 254 | executeSuccess = os.execute(command) 255 | end 256 | 257 | if executeSuccess == nil then 258 | log('transcode failed') 259 | 260 | if config.serveOriginalOnTranscodeFailure == true then 261 | log('serving original from: ' .. originalFilepath .. filename) 262 | ngx.exec('/luamp-cache', { luamp_cached_video_path = originalFilepath .. filename }) 263 | end 264 | else 265 | -- check if transcoded file is > minimumTranscodedFileSize 266 | -- we do this inside the transcoding `if` block to not mess with other threads 267 | local transcodedFile = io.open(cachedFilepath .. filename, 'rb') 268 | local transcodedFileSize = transcodedFile:seek('end') 269 | transcodedFile:close() 270 | 271 | if transcodedFileSize > config.minimumTranscodedFileSize then 272 | log('transcoded version is good, serving it') 273 | -- serve it 274 | ngx.exec('/luamp-cache', { luamp_cached_video_path = cachedFilepath .. filename }) 275 | else 276 | log('transcoded version is corrupt') 277 | -- delete corrupt one 278 | os.remove(cachedFilepath .. filename) 279 | 280 | -- serve original 281 | if config.serveOriginalOnTranscodeFailure == true then 282 | log('serving original') 283 | ngx.exec('/luamp-cache', { luamp_cached_video_path = originalFilepath .. filename }) 284 | end 285 | end 286 | end 287 | else 288 | log('found previously transcoded version, serving it') 289 | cachedFile:close() 290 | end 291 | 292 | ngx.exec('/luamp-cache', { luamp_cached_video_path = cachedFilepath .. filename }) 293 | --------------------------------------------------------------------------------