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 © 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.