├── .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 | 
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 | });
--------------------------------------------------------------------------------