├── .gitignore ├── videos └── SampleVideo_1280x720_1mb.mp4 ├── images └── video streaming in safari.png ├── package.json ├── public └── index.html ├── README.MD └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /videos/SampleVideo_1280x720_1mb.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootstrapping-microservices/video-streaming-example/HEAD/videos/SampleVideo_1280x720_1mb.mp4 -------------------------------------------------------------------------------- /images/video streaming in safari.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootstrapping-microservices/video-streaming-example/HEAD/images/video streaming in safari.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video-streaming-example", 3 | "version": "1.0.0", 4 | "description": "A Node.js/Express example of streaming video that works in both Chrome and Safari", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "start:dev": "nodemon index.js" 9 | }, 10 | "keywords": [], 11 | "author": "ashley@codecapers.com.au", 12 | "license": "MIT", 13 | "dependencies": { 14 | "express": "^4.17.1" 15 | }, 16 | "devDependencies": { 17 | "nodemon": "^2.0.4" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Video streaming example 6 | 7 | 8 |

Video streaming example

9 | 10 |
11 |
12 |
13 |

Works in Chrome (but not Safari):

14 |
15 | 24 |
25 | 26 |
27 |
28 |

Works in Chrome and Safari:

29 |
30 | 39 |
40 |
41 | 42 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Video streaming example 2 | 3 | A Node.js/Express example that demonstrates streaming video that works in both Chrome and Safari. 4 | 5 | You need Node.js installed to try out this example. 6 | 7 | This examples accompanies an upcoming blog post on video streaming. Please check back later for a link to the blog post. 8 | 9 | To learn about a complete microservices project based on video streaming please see my book [Bootstrapping Microservices](https://www.bootstrapping-microservices.com). 10 | 11 | [Click here to support my work](https://www.codecapers.com.au/about#support-my-work) 12 | 13 | ## Chrome vs Safari 14 | 15 | This images gives you an idea of the differences between video streaming for Chrome and Safari: 16 | 17 | ![Chrome vs Safari](images/video%20streaming%20in%20safari.png) 18 | 19 | ## Setup 20 | 21 | Download [the zip file for this respository](https://github.com/bootstrapping-microservices/video-streaming-example/archive/master.zip) or clone it using Git: 22 | 23 | ```bash 24 | git clone https://github.com/bootstrapping-microservices/video-streaming-example.git 25 | ``` 26 | 27 | Then install depdencies: 28 | 29 | ```bash 30 | cd video-streaming-example 31 | npm install 32 | ``` 33 | 34 | ## To start 35 | 36 | Start it normally: 37 | 38 | ```bash 39 | npm start 40 | ``` 41 | 42 | Or start with live reload enabled: 43 | 44 | ```bash 45 | npm run start:dev 46 | ``` 47 | 48 | Point your browser at http://localhost:3000 for the UI. 49 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const fs = require("fs"); 3 | 4 | const app = express(); 5 | 6 | const port = 3000; 7 | 8 | app.use(express.static("public")); 9 | 10 | const filePath = "./videos/SampleVideo_1280x720_1mb.mp4"; 11 | 12 | app.get("/works-in-chrome", (req, res) => { 13 | res.setHeader("content-type", "video/mp4"); 14 | 15 | fs.stat(filePath, (err, stat) => { 16 | if (err) { 17 | console.error(`File stat error for ${filePath}.`); 18 | console.error(err); 19 | res.sendStatus(500); 20 | return; 21 | } 22 | 23 | res.setHeader("content-length", stat.size); 24 | 25 | const fileStream = fs.createReadStream(filePath); 26 | fileStream.on("error", error => { 27 | console.log(`Error reading file ${filePath}.`); 28 | console.log(error); 29 | res.sendStatus(500); 30 | }); 31 | 32 | fileStream.pipe(res) 33 | }); 34 | }); 35 | 36 | app.get('/works-in-chrome-and-safari', (req, res) => { 37 | 38 | const options = {}; 39 | 40 | let start; 41 | let end; 42 | 43 | const range = req.headers.range; 44 | if (range) { 45 | const bytesPrefix = "bytes="; 46 | if (range.startsWith(bytesPrefix)) { 47 | const bytesRange = range.substring(bytesPrefix.length); 48 | const parts = bytesRange.split("-"); 49 | if (parts.length === 2) { 50 | const rangeStart = parts[0] && parts[0].trim(); 51 | if (rangeStart && rangeStart.length > 0) { 52 | options.start = start = parseInt(rangeStart); 53 | } 54 | const rangeEnd = parts[1] && parts[1].trim(); 55 | if (rangeEnd && rangeEnd.length > 0) { 56 | options.end = end = parseInt(rangeEnd); 57 | } 58 | } 59 | } 60 | } 61 | 62 | res.setHeader("content-type", "video/mp4"); 63 | 64 | fs.stat(filePath, (err, stat) => { 65 | if (err) { 66 | console.error(`File stat error for ${filePath}.`); 67 | console.error(err); 68 | res.sendStatus(500); 69 | return; 70 | } 71 | 72 | let contentLength = stat.size; 73 | 74 | if (req.method === "HEAD") { 75 | res.statusCode = 200; 76 | res.setHeader("accept-ranges", "bytes"); 77 | res.setHeader("content-length", contentLength); 78 | res.end(); 79 | } 80 | else { 81 | let retrievedLength; 82 | if (start !== undefined && end !== undefined) { 83 | retrievedLength = (end+1) - start; 84 | } 85 | else if (start !== undefined) { 86 | retrievedLength = contentLength - start; 87 | } 88 | else if (end !== undefined) { 89 | retrievedLength = (end+1); 90 | } 91 | else { 92 | retrievedLength = contentLength; 93 | } 94 | 95 | res.statusCode = start !== undefined || end !== undefined ? 206 : 200; 96 | 97 | res.setHeader("content-length", retrievedLength); 98 | 99 | if (range !== undefined) { 100 | res.setHeader("content-range", `bytes ${start || 0}-${end || (contentLength-1)}/${contentLength}`); 101 | res.setHeader("accept-ranges", "bytes"); 102 | } 103 | 104 | const fileStream = fs.createReadStream(filePath, options); 105 | fileStream.on("error", error => { 106 | console.log(`Error reading file ${filePath}.`); 107 | console.log(error); 108 | res.sendStatus(500); 109 | }); 110 | 111 | fileStream.pipe(res); 112 | } 113 | }); 114 | }); 115 | 116 | app.listen(port, () => { 117 | console.log(`Open your browser and navigate to http://localhost:${port}`) 118 | }); --------------------------------------------------------------------------------