diff --git a/restreamer/restreamer/main.py b/restreamer/restreamer/main.py
index a198831..04c77df 100644
--- a/restreamer/restreamer/main.py
+++ b/restreamer/restreamer/main.py
@@ -411,6 +411,11 @@ def cut(channel, quality):
 		stream, muxer, mimetype = (True, 'mpegts', 'video/MP2T') if type == 'mpegts' else (False, 'mp4', 'video/mp4')
 		encoding_args = ['-c:v', 'libx264', '-preset', 'ultrafast', '-crf', '0', '-f', muxer]
 		return Response(full_cut_segments(segment_ranges, ranges, transitions, encoding_args, stream=stream), mimetype=mimetype)
+	elif type == 'webm':
+		# Basic webm settings to work in Firefox and Chrome.
+		stream, muxer, mimetype = (True, 'webm', 'video/webm')
+		encoding_args = ['-c:v', 'libvpx-vp9', '-c:a', 'libopus', '-f', muxer]
+		return Response(full_cut_segments(segment_ranges, ranges, transitions, encoding_args, stream=stream), mimetype=mimetype)
 	else:
 		return "Unknown type {!r}".format(type), 400
 
diff --git a/thrimbletrimmer/scripts/edit.js b/thrimbletrimmer/scripts/edit.js
index 5e976cf..99944db 100644
--- a/thrimbletrimmer/scripts/edit.js
+++ b/thrimbletrimmer/scripts/edit.js
@@ -1655,6 +1655,43 @@ function updateDownloadLink() {
 		videoInfo.video_quality,
 	);
 	document.getElementById("download-link").href = downloadURL;
+
+	// Create preview links for each transition
+	const transitionPreviewPaddingSeconds = 5;
+	const transitionLinks = document.getElementsByClassName("range-transition-preview-button");
+	for (let i=0; i<transitionLinks.length; i++) {
+		const transitionLinkTarget = transitionLinks[i];
+		const previewTimeRanges = timeRanges.slice(i,i+2); // Current and next range
+		const previewTransition = transitions[i]; // Transition after current range
+		// Calculate the duration of the transition. Cut transitions have
+		// previewTransition == "" and are instantaneous.
+		const thisTransitionDuration = previewTransition ? parseFloat(previewTransition.split(",")[1]) : 0;
+		// Each side of the transition needs the configured padding length plus
+		// the duration of the transition.
+		const thisPaddingDurationSeconds = thisTransitionDuration + transitionPreviewPaddingSeconds;
+		if (previewTimeRanges[0].start && previewTimeRanges[0].end && previewTimeRanges[1].start && previewTimeRanges[1].end) {
+			// Adjust segment start and end times based on calculated padding.
+			previewTimeRanges[0].start = wubloaderTimeFromDateTime(luxon.DateTime.max(
+				dateTimeFromWubloaderTime(previewTimeRanges[0].start),
+				dateTimeFromWubloaderTime(previewTimeRanges[0].end).minus(luxon.Duration.fromMillis(thisPaddingDurationSeconds*1000))
+			));
+			previewTimeRanges[1].end = wubloaderTimeFromDateTime(luxon.DateTime.min(
+				dateTimeFromWubloaderTime(previewTimeRanges[1].end),
+				dateTimeFromWubloaderTime(previewTimeRanges[1].start).plus(luxon.Duration.fromMillis(thisPaddingDurationSeconds*1000))
+			));
+			// Create a video URL, forcing webm and 480p
+			const previewURL = generateDownloadURL(
+				previewTimeRanges,
+				[previewTransition],
+				"webm",
+				allowHoles,
+				"480p",
+			);
+			transitionLinkTarget.dataset.videoSource = previewURL;
+		} else {
+			transitionLinkTarget.dataset.videoSource = null;
+		}
+	}
 }
 
 async function setManualVideoLink() {
@@ -1819,7 +1856,39 @@ function rangeDefinitionDOM() {
 		handleFieldChange();
 	});
 	transitionDurationSection.append(" over ", transitionDuration, " seconds");
-	transitionContainer.append("Transition: ", transitionType, transitionDurationSection);
+
+	// Add a link to preview this transition
+	const transitionPreviewSection = makeElement("div", [
+		"range-transition-preview-section",
+	]);
+	const transitionPreviewButton = makeElement("button", ["range-transition-preview-button"]);
+	transitionPreviewButton.append("Preview");
+	transitionPreviewButton.addEventListener("click", (event) => {
+		const videoUrl = event.target.dataset.videoSource;
+
+		// Pop up a new window to contain the video preview.
+		// These dimensions aren't quite right because if I make them exact for 480p,
+		// firefox creates an unnecessary scroll bar.
+		const newWindow = window.open("", "_blank", "width=845,height=480");
+		// Very basic video player wrapper
+		newWindow.document.write(`
+		  <!DOCTYPE html>
+		  <html>
+			<head>
+			  <title>Thrimbletrimmer Transition Preview</title>
+			</head>
+			<body style="margin:0; padding:0;">
+			  <video width="100%" height="100%" controls autoplay>
+				<source src="${videoUrl}" type="video/webm" />
+				Your browser does not support the video tag.
+			  </video>
+			</body>
+		  </html>
+		`);
+		newWindow.document.close();
+	});
+	transitionPreviewSection.append(transitionPreviewButton);
+	transitionContainer.append("Transition: ", transitionType, transitionDurationSection, transitionPreviewSection);
 
 	const rangeTimesContainer = makeElement("div", ["range-definition-times"]);
 	const rangeStart = makeElement("input", ["range-definition-start"], { type: "text" });
diff --git a/thrimbletrimmer/styles/thrimbletrimmer.css b/thrimbletrimmer/styles/thrimbletrimmer.css
index 005381a..f3e0c1b 100644
--- a/thrimbletrimmer/styles/thrimbletrimmer.css
+++ b/thrimbletrimmer/styles/thrimbletrimmer.css
@@ -248,12 +248,18 @@ a.click {
 
 .range-transition-duration-section {
 	display: inline-block;
+	padding-left: 0.5em;
 }
 
 .range-transition-duration {
 	width: 50px;
 }
 
+.range-transition-preview-section {
+	display: inline-block;
+	padding-left: 0.5em;
+}
+
 .range-definition-times {
 	display: flex;
 	align-items: center;