Rewind/forward of embed video in node.js

Following suggestion from: Scripts in Tiddlywiki — codes, macros, and solutions in TW, I can embed a local video into my TiddlyWiki 5.2.3 with node.js in Windows 10. However, rewind/forward is working for Firefox, but not for Chrome and Edge.

The video files are stored under files/video/.

Then I test a single file TW through export into a static file which don’t have any problem in Chrome and Edge.

How could I solve this problem in Chrome?

can you share the code you are using?

In node.js, I used codes below flower.mp4 is stored under files/video.


<div style="text-align:center;"> 
<video controls width="100%">
   <source src="./files/video/flower.mp4" type="video/mp4">
Sorry, your browser doesn't support embedded videos.
</video>
</div>
\end

I also tested single file version.

<div style="text-align:center;"> 
<video controls width="100%">
   <source src="flower.mp4" type="video/mp4">
Sorry, your browser doesn't support embedded videos.
</video>
</div>
  • It is working with rewind and forward in Chrome as I expected.

After googling, it seems it is related with ranged requests. Could we fix it? I have embedded lots of local video of meeting records. It would be very useful for me.

1 Like

I’ve encountered the same issue as you did when I try to play mp4 video in <video> tag embedded in a tiddler under Safari browser or Tauri Webview App (I’m using macOS Ventura 13.7.4 / 22H420).

This is a quick-n-dirty patch on source file tiddlywiki/core/modules/server/server.js (based on version 5.3.6, around line 300), I hope someone could provide more elegant solution. (PLEASE do not use this solution in production code. It’s written by Perplexity.ai and not tested by seasoned developer.)

	// Receive the request body if necessary and hand off to the route handler
	if(route.bodyFormat === "stream" || request.method === "GET" || request.method === "HEAD") {
		// List filename extensions and MIME names we need as a dictionary. 
		const MIME_TYPES = {
			'.mp3': 'audio/mpeg',
			'.mp4': 'video/mp4',
			'.ogg': 'application/ogg', 
			'.ogv': 'video/ogg', 
			'.oga': 'audio/ogg',
			'.wav': 'audio/x-wav',
			'.webm': 'video/webm'
		};
		// Assume route.handler is a function that takes (request, response, state)
		// We'll wrap or replace it here with range handling for media files
		const range = request.headers.range;
		const filePath = path.join(process.cwd(), this.boot.wikiPath, request.url);
		const ext = path.extname(filePath).toLowerCase();
		// Only handle range requests for files witin MIME_TYPES
		if ((ext in MIME_TYPES) && range) {
			fs.stat(filePath, (err, stats) => {
				if (err) {
					response.writeHead(404);
					return response.end();
				}
				const total = stats.size;
				const parts = range.replace(/bytes=/, "").split("-");
				const start = parseInt(parts[0], 10);
				const end = parts[1] ? parseInt(parts[1], 10) : total - 1;
				// Validate range
				if (start >= total || end >= total) {
					response.writeHead(416, {
						'Content-Range': `bytes */${total}`
					});
					return response.end();
				}
				const chunkSize = (end - start) + 1;
				response.writeHead(206, {
					'Content-Range': `bytes ${start}-${end}/${total}`,
					'Accept-Ranges': 'bytes',
					'Content-Length': chunkSize,
					'Content-Type': MIME_TYPES[ext],
				});
				const stream = fs.createReadStream(filePath, { start, end });
				stream.pipe(response);
			});
		} else {
			// No range header or not a media file, proceed normally
			route.handler(request, response, state);
		}
	} else if(route.bodyFormat === "string" || !route.bodyFormat) {
	...