PageSourceSearch

https://www.videostitchapp.com/cdn-cgi/l/email-protection

html videostitchapp.com collected 2026-09-24 10:06:52 UTC 29,117 bytes, 940 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3
4<head>
5  <meta charset="UTF-8" />
6  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7  <meta name="keywords" content="video, stitch, editor, free, online, privacy, client-side, web app, PWA, transitions, music overlay, video editor, online video editor" />
8  <meta name="description" content="Stitch two videos together with music and transitions — 100% private, client-side processing. Your footage never leaves your device. Free, no account required." />
9  <link rel="apple-touch-icon" sizes="180x180" href="./ios/apple-touch-icon.png">
10  <!-- <link rel="manifest" href="./manifest.webmanifest"> -->
11  <link rel="manifest" href="./manifest.webmanifest">
12  <link rel="canonical" href="https://www.videostitchapp.com/" />
13  <title>Video Stitch</title>
14  <style>
15    body {
16      border: 0px solid black;
17      align-items: center;
18      background: #fff;
19      color: #000;
20      display: flex;
21      flex-direction: column;
22      font-family: "Segoe UI", sans-serif;
23      margin: 0;
24      min-width: 100%;
25    }
26
27    .card {
28      border: 0px solid yellow;
29      background: white;
30      border-radius: 15px;
31      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
32      font-family: "Segoe UI", sans-serif;
33      padding: 0rem;
34
35      /* The Magic Line: width tries for 100%, but never < 320px or > 800px */
36      width: clamp(320px, 100vw, 800px);
37
38      /* Prevents the card from overflowing the screen on tiny devices */
39      box-sizing: border-box;
40    }
41
42    header {
43      border: 0px solid orange;
44      align-items: center;
45      display: flex;
46      flex-wrap: wrap;
47      gap: 15px;
48      height: auto;
49      justify-content: space-between;
50      padding: 1rem;
51    }
52
53    .appname {
54      border: 0px solid greenyellow;
55      color: #007bff;
56      flex: 1 1 auto;
57      font-size: 2.0rem;
58      font-weight: bold;
59      min-width: 200px;
60    }
61
62    .tagline {
63      border: 0px solid cyan;
64      color: #000;
65      font-size: 1.1rem;
66      flex: 2 1 300px;
67    }
68
69    .pwa-swap-container {
70      border: 0px solid red;
71      align-items: center;
72      display: flex;
73      flex: 0 0 auto;
74      gap: 10px;
75    }
76
77    .pwa-icon {
78      float: right;
79      height: 3rem;
80      width: 3rem;
81    }
82
83    #swapBtn {
84      background: lightblue;
85      height: auto;
86      padding: 10px 15px;
87      width: auto;
88    }
89
90    /* Each individual card */
91    .input-row {
92      background: #fafafa;
93      border-left: 5px solid #007bff;
94      border-radius: 0 10px 10px 0;
95      margin-bottom: 20px;
96      padding: 15px;
97    }
98
99    label {
100      display: block;
101      font-weight: bold;
102      margin-bottom: 10px;
103    }
104
105    video {
106      background: #000;
107      border-radius: 8px;
108      display: none;
109      margin-top: 10px;
110      max-width: 320px;
111      width: 100%;
112    }
113
114    .drop-zone {
115      border: 2px dashed #007bff;
116      border-radius: 12px;
117      padding: 20px;
118      text-align: center;
119      transition: all 0.3s ease;
120      cursor: pointer;
121      background: #f8fbff;
122      position: relative;
123      min-height: 100px;
124      display: flex;
125      flex-direction: column;
126      align-items: center;
127      justify-content: center;
128    }
129
130    .drop-zone--over {
131      background-color: #e7f3ff;
132      border-style: solid;
133      transform: scale(1.01);
134    }
135
136    .drop-zone input[type="file"] {
137      display: none;
138    }
139
140    .drop-zone__prompt {
141      color: #000;
142      font-weight: 500;
143    }
144
145    /* Video Inside Drop Zone */
146    .drop-zone video {
147      width: 100%;
148      max-width: 400px;
149      max-height: 250px;
150      display: none;
151      border-radius: 8px;
152      margin-top: 10px;
153      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
154    }
155
156    /* Hide prompt when video is loaded */
157    .drop-zone.has-video .drop-zone__prompt {
158      font-size: 0.8rem;
159      color: #888;
160      margin-bottom: 5px;
161    }
162
163    .vidwarningtext {
164      color: #888;
165      font-size: 0.9rem;
166      margin-top: 5px;
167    }
168
169    /* ADDED PROGRESS BAR STYLES BEGIN*/
170    #progressBarContainer {
171      background: #f0f0f0;
172      border-radius: 10px;
173      height: 20px;
174      /* Slightly thicker for better visibility */
175      margin: 25px 0;
176      overflow: hidden;
177      box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.1);
178      width: 100%;
179    }
180
181    #progressBar {
182      background: linear-gradient(90deg, #007bff, #00c6ff, #007bff);
183      background-size: 200% 100%;
184      /* Required for the shimmer effect */
185      height: 100%;
186      width: 0%;
187      transition: width 0.4s ease;
188      /* Smoother movement */
189
190      /* The "Working" Animation */
191      animation: shimmer 2s infinite linear;
192    }
193
194    @keyframes shimmer {
195      0% {
196        background-position: 200% 0;
197      }
198
199      100% {
200        background-position: -200% 0;
201      }
202    }
203
204    /* ADDED PROGRESS BAR STYLES END*/
205
206    .controls {
207      display: grid;
208      gap: 20px;
209      grid-template-columns: 1fr 1fr;
210    }
211
212    button {
213      border: none;
214      border-radius: 10px;
215      cursor: pointer;
216      font-weight: bold;
217      padding: 14px;
218      transition: all 0.2s;
219    }
220
221    #startBtn {
222      background: #007bff;
223      color: white;
224      font-size: 1.2rem;
225      width: 100%;
226    }
227
228    #startBtn:disabled {
229      background: #ccc;
230      cursor: not-allowed;
231    }
232
233    #status {
234      /* color: #555; */
235      color: black;
236      font-weight: 500;
237      margin-top: 15px;
238      text-align: center;
239    }
240
241    .download-btn {
242      background: #28a745;
243      border-radius: 30px;
244      color: white;
245      display: block;
246      font-weight: bold;
247      text-align: center;
248      margin: 25px auto;
249      padding: 15px 30px;
250      text-decoration: none;
251      width: fit-content;
252    }
253
254    #outputVideo {
255      border-radius: 12px;
256      box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
257      display: none;
258      margin: 20px auto;
259      max-width: 100%;
260    }
261
262    select,
263    input[type="range"] {
264      border: 1px solid #ddd;
265      border-radius: 5px;
266      padding: 8px;
267      width: 100%;
268    }
269
270    #qualityMode {
271      margin-bottom: 10px;
272    }
273
274    h2 {
275      color: #007bff;
276      margin: 0;
277    }
278
279    #bgMusic {
280      margin-bottom: 10px;
281    }
282
283    #bgVol {
284      width: auto;
285    }
286
287    footer {
288      color: #000;
289      font: 0.9rem "Segoe UI", sans-serif;
290      margin: 30px 30px;
291      text-align: center;
292    }
293
294    .footer-legal {
295      margin-top: 10px;
296      margin-left: 1em;
297      margin-right: 1em;
298      font-size: 0.85rem;
299    }
300
301    .footer-legal a {
302      margin: 0 6px;
303    }
304
305    .footer-legal a:hover {
306      text-decoration: underline;
307    }
308
309    .footer-divider {
310      color: #aaa;
311    }
312
313    /* ---- Store availability badges ---- */
314    .store-badges {
315      margin-top: 16px;
316      padding-top: 14px;
317      border-top: 1px solid #e8e8e8;
318    }
319    .store-badges-label {
320      display: block;
321      font-size: 0.78rem;
322      color: #000;
323      margin-bottom: 10px;
324      letter-spacing: 0.03em;
325    }
326    .store-badges-row {
327      display: flex;
328      gap: 10px;
329      align-items: center;
330      flex-wrap: wrap;
331      justify-content: center;
332    }
333    .store-badge-link {
334      display: inline-block;
335      opacity: 0.88;
336      transition: opacity 0.2s;
337    }
338    .store-badge-link:hover { opacity: 1; }
339    .store-badge-img {
340      height: 40px;
341      width: auto;
342      display: block;
343    }
344    .store-badge-link img[alt="Get it from Microsoft"] {
345      height: 40px;
346      width: auto;
347    }
348
349    /* ---- About / Contact modals ---- */
350    .vs-info-backdrop {
351      display: none;
352      position: fixed;
353      inset: 0;
354      background: rgba(0,0,0,0.55);
355      z-index: 9998;
356      justify-content: center;
357      align-items: center;
358      animation: vsOverlayIn 0.2s ease;
359    }
360    .vs-info-backdrop.active { display: flex; }
361
362    .vs-info-box {
363      background: #fff;
364      border-radius: 12px;
365      padding: 1.6rem;
366      width: min(92vw, 420px);
367      position: relative;
368      box-shadow: 0 8px 40px rgba(0,0,0,0.2);
369      max-height: 80vh;
370      overflow-y: auto;
371    }
372    .vs-info-box h3 {
373      color: #007bff;
374      font-size: 1.1rem;
375      font-weight: 700;
376      margin: 0 0 0.9rem;
377    }
378    .vs-info-box p {
379      color: #444;
380      font-size: 0.88rem;
381      line-height: 1.7;
382      margin: 0 0 0.75rem;
383    }
384    .vs-info-box p:last-of-type { margin-bottom: 0; }
385    .vs-info-box a { color: #007bff; text-decoration: none; }
386    .vs-info-box a:hover { text-decoration: underline; }
387    .vs-info-close {
388      position: absolute;
389      top: 0.9rem; right: 1rem;
390      background: none; border: none;
391      font-size: 1.1rem; color: #aaa;
392      cursor: pointer; padding: 4px;
393      border-radius: 4px;
394      transition: color 0.15s;
395      line-height: 1;
396    }
397    .vs-info-close:hover { color: #333; }
398
399    /* ---- About / How It Works section ---- */
400    .about-section {
401      border-top: 1px solid #e8e8e8;
402      margin: 10px 0 0;
403      padding: 24px 20px 28px;
404      /* background: #fafafa; */
405      background: white;
406      border-radius: 0 0 15px 15px;
407    }
408    .about-section h2 {
409      /* color: #007bff; */
410      color: black;
411      font-size: 1.15rem;
412      font-weight: 700;
413      margin: 0 0 10px;
414    }
415    .about-section p {
416      color: #444;
417      font-size: 0.92rem;
418      line-height: 1.65;
419      margin: 0 0 12px;
420    }
421    .about-section ul {
422      margin: 0;
423      padding-left: 1.2rem;
424      color: #444;
425      font-size: 0.92rem;
426      line-height: 1.8;
427    }
428    .about-section ul li::marker {
429      /* color: #007bff; */
430      color: black;
431    }
432    .how-it-works {
433      color: black;
434    }
435
436    /* ---- Intro paragraph (above the fold) ---- */
437    .intro-section {
438      padding: 0 20px 18px;
439      border-bottom: 1px solid #e8e8e8;
440    }
441    .intro-section p {
442      color: #333;
443      font-size: 0.92rem;
444      line-height: 1.7;
445      margin: 0;
446    }
447
448    /* ---- FAQ ---- */
449    .faq-section {
450      padding: 20px 20px 24px;
451      border-top: 1px solid #e8e8e8;
452    }
453    .faq-section h2 {
454      color: black;
455      font-size: 1.1rem;
456      font-weight: 700;
457      margin: 0 0 14px;
458    }
459    .faq-item {
460      margin-bottom: 14px;
461    }
462    .faq-item:last-child { margin-bottom: 0; }
463    .faq-item dt {
464      font-weight: 700;
465      font-size: 0.9rem;
466      color: #222;
467      margin-bottom: 4px;
468    }
469    .faq-item dd {
470      margin: 0;
471      font-size: 0.88rem;
472      color: #555;
473      line-height: 1.65;
474    }
475  </style>
476</head>
477
478<body>
479  <main class="card">
480    <header>
481      <h2 class="appname">VideoStitch</h2>
482      <div class="tagline">
483        Combine Two Videos Seamlessly! 
484        <a href="/cdn-cgi/l/email-protection#f891969e97b8d68f978a949c8f919c9d8f9d9a888a979f8a9995959d8a8bd69b9795">Contact</a>
485      </div>
486      <div>
487        Privacy by design. Everything's procesed locally in your browser. No uploads, no data collection. Just pure client-side video magic! 🔒 ✨
488      </div>
489      <div class="pwa-swap-container">
490        <svg class="pwa-icon">
491          <use href="pwa-icon.svg" alt="pwa image" />
492        </svg>
493        <button id="swapBtn" type="button">
494          ⇄ Swap Videos
495        </button>
496      </div>
497    </header>
498
499    <!-- Intro: crawlable above-the-fold content for AdSense / SEO -->
500    <div class="intro-section">
501      <p>VideoStitch is a <strong>free, browser-based video editor</strong> that
502      lets you combine two video clips, add background music, apply transitions,
503      and overlay your logo — all without uploading a single file. Everything
504      runs locally on your device using WebAssembly, so your footage stays
505      completely private. Works on iPhone, Android, and PC with no installation
506      required.</p>
507    </div>
508
509    <div class="input-row">
510      <label>Video 1 (Primary)</label>
511      <div class="drop-zone" id="dz1">
512        <span class="drop-zone__prompt">Drag & Drop or Click to Upload</span>
513        <input type="file" id="video1" accept="video/*" />
514        <video id="preview1" controls></video>
515      </div>
516    </div>
517
518    <div class="input-row">
519      <label>Video 2 (Secondary)</label>
520      <div class="drop-zone" id="dz2">
521        <span class="drop-zone__prompt">Drag & Drop or Click to Upload</span>
522        <input type="file" id="video2" accept="video/*" />
523        <video id="preview2" controls></video>
524      </div>
525    </div>
526
527    <div class="controls">
528      <div class="input-row">
529        <label for="bgMusic">Music File</label>
530        <input type="file" id="bgMusic" accept="audio/mp3" />
531        <label for="bgVol">Volume</label>
532        <input type="range" id="bgVol" min="0" max="1" step="0.1" value="0.5" /></label>
533      </div>
534      <div class="input-row">
535        <label>Settings & Quality</label>
536        <select id="qualityMode" title="qualityMode">
537          <option value="854:480">480p - Fast Render</option>
538          <option value="1280:720">720p - High Quality</option>
539        </select>
540        <select id="transitionType" title="transitionType">
541          <option value="fade">Cross Fade</option>
542          <option value="wipeleft">Wipe Left</option>
543          <option value="wiperight">Wipe Right</option>
544        </select>
545      </div>
546    </div>
547    <div class="input-row">
548      <label for="logoFile">Overlay Logo (Optional)</label>
549      <input type="file" id="logoFile" accept="image/*" />
550    </div>
551
552    <button id="startBtn" type="button">Generate Final Video</button>
553    <div id="progressBarContainer">
554      <div id="progressBar"></div>
555    </div>
556    <p id="status">Waiting for files...</p>
557    <div id="downloadArea"></div>
558    <video id="outputVideo" controls></video>
559
560    <!-- About + FAQ: crawlable content for AdSense / SEO -->
561    <section class="about-section">
562      <h2 class="how-it-works">How VideoStitch Works</h2>
563      <p>VideoStitch is a free, privacy-first video editor that runs entirely
564      in your browser. No uploads, no account required — your videos never
565      leave your device.</p>
566      <ul>
567        <li>Combine two videos with smooth crossfade or wipe transitions</li>
568        <li>Add a background music soundtrack with adjustable volume</li>
569        <li>Overlay your logo or watermark on the final video</li>
570        <li>Export in 480p or 720p — all processed locally via WebAssembly</li>
571      </ul>
572    </section>
573
574    <section class="faq-section">
575      <h2>Frequently Asked Questions</h2>
576      <dl>
577        <div class="faq-item">
578          <dt>Is VideoStitch really free?</dt>
579          <dd>Yes — VideoStitch is completely free to use with no subscription,
580          no account, and no hidden fees.</dd>
581        </div>
582        <div class="faq-item">
583          <dt>Do my videos get uploaded to a server?</dt>
584          <dd>No. All video processing happens entirely in your browser using
585          WebAssembly. Your footage never leaves your device.</dd>
586        </div>
587        <div class="faq-item">
588          <dt>What video formats are supported?</dt>
589          <dd>VideoStitch accepts most common formats including MP4, MOV, and
590          WebM. The output is always exported as MP4 for maximum compatibility.</dd>
591        </div>
592        <div class="faq-item">
593          <dt>Why does processing take a while?</dt>
594          <dd>Video encoding is computationally intensive. Because everything
595          runs locally in your browser rather than on a server, processing time
596          depends on your device's speed and the length of your clips.</dd>
597        </div>
598        <div class="faq-item">
599          <dt>Does VideoStitch work on mobile?</dt>
600          <dd>Yes — VideoStitch is a Progressive Web App (PWA) that works on
601          iPhone, Android, and desktop browsers with no installation needed.</dd>
602        </div>
603      </dl>
604    </section>
605  </main>
606  <footer>
607    <span class="copyright">Copyright &copy;
608      
608<script>
609        document.write(new Date().getFullYear());
610      </script>
610
611      <a href="https://worldwidewebprogrammers.com" rel="noopener" target="_blank">Worldwide Web Programmers</a>. All Rights Reserved.
612    </span>
613    <div class="footer-legal">
614      <a href="terms.html">Terms of Service</a>
615      <span class="footer-divider">|</span>
616      <a href="privacy.html">Privacy Policy</a>
617      <span class="footer-divider">|</span>
618      <button id="vs-about-btn"
619        style="background:none;border:none;cursor:pointer;font:inherit;color:inherit;font-size:0.85rem;">
620        About Us
621      </button>
622      <span class="footer-divider">|</span>
623      <button id="vs-contact-btn"
624        style="background:none;border:none;cursor:pointer;font:inherit;color:inherit;font-size:0.85rem;">
625        Contact Us
626      </button>
627    </div>
628
629    <!-- Store availability badges -->
630    <div class="store-badges" aria-label="Available on">
631      <span class="store-badges-label">Also available on</span>
632      <div class="store-badges-row">
633
634        <a href="https://play.google.com/store/apps/details?id=com.videostitchapp.twa" rel="noopener" title="Install VideoStitch on Android" target="_blank">
635          <img src="logo-googleplay.png" alt="Google Play Store badge" width="130"/>
636        </a>
637
638        <a href="https://apps.microsoft.com/detail/9NSXJSMT0GBR"
639          class="store-badge-link"
640          target="_blank"
641          rel="noopener"
642          aria-label="Get it from Microsoft">
643          <img
644            class="store-badge-ms"
645            src="https://developer.microsoft.com/store/badges/images/English_get-it-from-MS.png"
646            alt="Get it from Microsoft"/>
647        </a>
648      </div>
649    </div>
650  </footer>
651  
651<script src="https://unpkg.com/@ffmpeg/[email protected]/dist/ffmpeg.min.js"></script>
651
652  
652<script>
653    const { createFFmpeg, fetchFile } = FFmpeg;
654    const ffmpeg = createFFmpeg({ log: true });
655
656    // --- UTILS ---
657    const getDuration = (file) => {
658      return new Promise((resolve) => {
659        const video = document.createElement("video");
660        video.preload = "metadata";
661        video.onloadedmetadata = () => {
662          resolve(video.duration);
663          URL.revokeObjectURL(video.src);
664        };
665        video.src = URL.createObjectURL(file);
666      });
667    };
668
669    const sanitizeImage = (file) => {
670      return new Promise((resolve) => {
671        const img = new Image();
672        img.onload = () => {
673          const canvas = document.createElement("canvas");
674          canvas.width = img.width;
675          canvas.height = img.height;
676          const ctx = canvas.getContext("2d");
677          ctx.drawImage(img, 0, 0);
678          canvas.toBlob((blob) => {
679            const reader = new FileReader();
680            reader.onloadend = () => resolve(new Uint8Array(reader.result));
681            reader.readAsArrayBuffer(blob);
682          }, "image/png");
683        };
684        img.src = URL.createObjectURL(file);
685      });
686    };
687
688    // --- UI & CORE LOGIC ---
689    document.addEventListener("DOMContentLoaded", () => {
690      const v1Input = document.getElementById("video1");
691      const v2Input = document.getElementById("video2");
692      const p1 = document.getElementById("preview1");
693      const p2 = document.getElementById("preview2");
694
695      // 1. Initialize Drop Zones
696      document.querySelectorAll(".drop-zone").forEach((dropZone) => {
697        const input = dropZone.querySelector("input");
698        const preview = dropZone.querySelector("video");
699        const prompt = dropZone.querySelector(".drop-zone__prompt");
700
701        // Prevent browser from opening file on drop
702        ["dragenter", "dragover", "dragleave", "drop"].forEach(name => {
703          dropZone.addEventListener(name, (e) => {
704            e.preventDefault();
705            e.stopPropagation();
706          });
707        });
708
709        dropZone.addEventListener("click", () => input.click());
710        preview.addEventListener("click", (e) => e.stopPropagation());
711
712        dropZone.addEventListener("dragover", () => dropZone.classList.add("drop-zone--over"));
713        ["dragleave", "dragend"].forEach(type => {
714          dropZone.addEventListener(type, () => dropZone.classList.remove("drop-zone--over"));
715        });
716
717        dropZone.addEventListener("drop", (e) => {
718          dropZone.classList.remove("drop-zone--over");
719          if (e.dataTransfer.files.length) {
720            input.files = e.dataTransfer.files;
721            input.dispatchEvent(new Event("change"));
722          }
723        });
724
725        // Unified Change Listener for both Click and Drop
726        input.addEventListener("change", () => {
727          if (input.files[0]) {
728            const url = URL.createObjectURL(input.files[0]);
729            preview.src = url;
730            preview.style.display = "block";
731            dropZone.classList.add("has-video");
732            prompt.innerHTML = `<b>Selected:</b> ${input.files[0].name} <br> (Click to change)`;
733          }
734        });
735      });
736
737      // 2. Swap Button Logic
738      document.getElementById("swapBtn").onclick = () => {
739        const f1 = v1Input.files[0];
740        const f2 = v2Input.files[0];
741        if (!f1 && !f2) return;
742
743        const dt1 = new DataTransfer();
744        if (f2) dt1.items.add(f2);
745        const dt2 = new DataTransfer();
746        if (f1) dt2.items.add(f1);
747
748        v1Input.files = dt1.files;
749        v2Input.files = dt2.files;
750
751        // Manually trigger change to update the new internal previews
752        v1Input.dispatchEvent(new Event("change"));
753        v2Input.dispatchEvent(new Event("change"));
754      };
755
756      // 3. Start FFmpeg Processing
757      document.getElementById("startBtn").onclick = async () => {
758        const btn = document.getElementById("startBtn");
759        const status = document.getElementById("status");
760        const bar = document.getElementById("progressBar");
761        const outVid = document.getElementById("outputVideo");
762
763        const f1 = v1Input.files[0];
764        const f2 = v2Input.files[0];
765        const music = document.getElementById("bgMusic").files[0];
766        const logo = document.getElementById("logoFile").files[0];
767
768        if (!f1 || !f2) return alert("Please select two videos.");
769
770        try {
771          btn.disabled = true;
772          if (!ffmpeg.isLoaded()) await ffmpeg.load();
773
774          const dur1 = await getDuration(f1);
775          const dur2 = await getDuration(f2);
776          const fadeTime = 1;
777          const totalDur = (dur1 + dur2 - fadeTime).toFixed(2);
778          const offset = (dur1 - fadeTime).toFixed(2);
779
780          status.innerText = "Processing Assets...";
781
782          // Cleanup virtual FS
783          try {
784            ["v1.mp4", "v2.mp4", "m.mp3", "l.png", "out.mp4"].forEach(f => ffmpeg.FS("unlink", f));
785          } catch (e) { }
786
787          ffmpeg.FS("writeFile", "v1.mp4", await fetchFile(f1));
788          ffmpeg.FS("writeFile", "v2.mp4", await fetchFile(f2));
789          if (music) ffmpeg.FS("writeFile", "m.mp3", await fetchFile(music));
790
791          let hasLogo = false;
792          if (logo) {
793            const cleanLogo = await sanitizeImage(logo);
794            ffmpeg.FS("writeFile", "l.png", cleanLogo);
795            hasLogo = true;
796          }
797
798          const [resW, resH] = document.getElementById("qualityMode").value.split(":");
799          const trans = document.getElementById("transitionType").value;
800
801          let filter =
802            `[0:v]scale=${resW}:${resH}:force_original_aspect_ratio=decrease,pad=${resW}:${resH}:(ow-iw)/2:(oh-ih)/2,format=yuv420p,fps=30[v0];` +
803            `[1:v]scale=${resW}:${resH}:force_original_aspect_ratio=decrease,pad=${resW}:${resH}:(ow-iw)/2:(oh-ih)/2,format=yuv420p,fps=30[v1];` +
804            `[v0][v1]xfade=transition=${trans}:duration=${fadeTime}:offset=${offset},format=yuv420p[outv]`;
805
806          let finalA;
807          if (music) {
808            const vol = document.getElementById("bgVol").value;
809            filter += `;[2:a]aresample=44100,aloop=loop=-1:size=44100,volume=${vol}[outa]`;
810            finalA = "[outa]";
811          } else {
812            filter += `;[0:a]aresample=44100[a0];[1:a]aresample=44100[a1];[a0][a1]acrossfade=d=${fadeTime}[outa]`;
813            finalA = "[outa]";
814          }
815
816          if (hasLogo) {
817            const logoIdx = music ? "3" : "2";
818            filter += `;[${logoIdx}:v]scale=${Math.round(resW / 7)}:-1[l];[outv][l]overlay=W-w-15:15[outv]`;
819          }
820
821          status.innerText = "Rendering... (This may take a moment)";
822          ffmpeg.setProgress(({ ratio }) => {
823            bar.style.width = Math.round(ratio * 100) + "%";
824          });
825
826          await ffmpeg.run(
827            "-i", "v1.mp4", "-i", "v2.mp4",
828            ...(music ? ["-i", "m.mp3"] : []),
829            ...(hasLogo ? ["-i", "l.png"] : []),
830            "-filter_complex", filter,
831            "-map", "[outv]", "-map", finalA,
832            "-c:v", "libx264", "-profile:v", "main", "-level", "4.0",
833            "-pix_fmt", "yuv420p", "-preset", "ultrafast", "-crf", "28",
834            "-c:a", "aac", "-ar", "44100", "-ac", "2", "-b:a", "128k",
835            "-t", totalDur, "-movflags", "+faststart", "out.mp4"
836          );
837
838          const data = ffmpeg.FS("readFile", "out.mp4");
839          const url = URL.createObjectURL(new Blob([data.buffer], { type: "video/mp4" }));
840
841          outVid.src = url;
842          outVid.style.display = "block";
843          outVid.load();
844
845          const dl = document.createElement("a");
846          dl.href = url;
847          dl.download = "video-stitch.mp4";
848          dl.innerText = "Download Final Video";
849          dl.className = "download-btn";
850          document.getElementById("downloadArea").innerHTML = "";
851          document.getElementById("downloadArea").appendChild(dl);
852          status.innerText = "Export Complete!";
853        } catch (e) {
854          console.error(e);
855          status.innerText = "Error: " + e.message;
856        } finally {
857          btn.disabled = false;
858        }
859      };
860    });
861  </script>
861
862
863  <!-- Register service worker for PWA capabilities (caching, offline support, etc.) -->
864  
864<script>
865  if ('serviceWorker' in navigator) {
866    // Wait until the page is fully loaded to register (optional but recommended)
867    window.addEventListener('load', () => {
868      navigator.serviceWorker.register('./sw.js')
869        .then((registration) => {
870          console.log('Service worker registration succeeded with scope:', registration.scope);
871        })
872        .catch((error) => {
873          console.error(`Service worker registration failed: `, error);
874    });
875  });
876  }
877  </script>
877
878
879  <!-- Interstitial overlay: shown after download completes -->
880  <!-- About Us modal -->
881  <div id="vs-about-modal" class="vs-info-backdrop"
882       role="dialog" aria-modal="true" aria-labelledby="vs-about-title">
883    <div class="vs-info-box">
884      <button class="vs-info-close" id="vs-about-close" aria-label="Close">✕</button>
885      <h3 id="vs-about-title">🎬 About VideoStitch</h3>
886      <p>VideoStitch is a free, privacy-first video editor that runs entirely
887      in your browser. No uploads, no account required — your videos never
888      leave your device.</p>
889      <p>All processing is handled locally using WebAssembly and FFmpeg.wasm,
890      meaning your footage stays completely private. VideoStitch works on
891      iPhone, Android, and PC — no installation needed.</p>
892      <p>VideoStitch is developed and maintained by
893      <a href="https://worldwidewebprogrammers.com" target="_blank" rel="noopener"
894      >Worldwide Web Programmers</a>.</p>
895    </div>
896  </div>
897
898  <!-- Contact Us modal -->
899  <div id="vs-contact-modal" class="vs-info-backdrop"
900       role="dialog" aria-modal="true" aria-labelledby="vs-contact-title">
901    <div class="vs-info-box">
902      <button class="vs-info-close" id="vs-contact-close" aria-label="Close">✕</button>
903      <h3 id="vs-contact-title">✉️ Contact Us</h3>
904      <p>Have a question, found a bug, or want to suggest a feature?
905      We'd love to hear from you.</p>
906      <p>Reach us at:<br>
907      <a href="https://worldwidewebprogrammers.com" target="_blank" rel="noopener"
908      >worldwidewebprogrammers.com</a></p>
909    </div>
910  </div>
911
912  
912<script>
913    // ── About / Contact modal logic ───────────────────────────────────────────
914    (function () {
915      function initVsModal(openBtnId, modalId, closeBtnId) {
916        var openBtn  = document.getElementById(openBtnId);
917        var modal    = document.getElementById(modalId);
918        var closeBtn = document.getElementById(closeBtnId);
919        if (!openBtn || !modal || !closeBtn) return;
920
921        function open()  { modal.classList.add('active'); }
922        function close() { modal.classList.remove('active'); }
923
924        openBtn.addEventListener('click', open);
925        closeBtn.addEventListener('click', close);
926        modal.addEventListener('click', function (e) {
927          if (e.target === modal) close();
928        });
929        document.addEventListener('keydown', function (e) {
930          if (e.key === 'Escape' && modal.classList.contains('active')) close();
931        });
932      }
933
934      initVsModal('vs-about-btn',   'vs-about-modal',   'vs-about-close');
935      initVsModal('vs-contact-btn', 'vs-contact-modal', 'vs-contact-close');
936    })();
937  </script>
937
938
939</body>
940</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.