fork download
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>MelindaAI AutoVideo - Advanced Video Creator</title>
  6. <!-- Include GSAP from CDN -->
  7. <script src="https://c...content-available-to-author-only...e.com/ajax/libs/gsap/3.11.5/gsap.min.js"></script>
  8. <!-- Include CCapture.js from CDN -->
  9. <script src="https://c...content-available-to-author-only...r.net/npm/ccapture.js@1.1.10/build/CCapture.all.min.js"></script>
  10. <!-- Include ffmpeg.wasm from CDN -->
  11. <script src="https://c...content-available-to-author-only...r.net/npm/@ffmpeg/ffmpeg@0.11.5/dist/ffmpeg.min.js"></script>
  12. <style>
  13. body {
  14. margin: 0;
  15. font-family: Arial, sans-serif;
  16. background-color: #f9fafb;
  17. }
  18. #app {
  19. display: flex;
  20. flex-direction: column;
  21. height: 100vh;
  22. }
  23. background-color: #2d3748;
  24. color: #edf2f7;
  25. padding: 20px;
  26. text-align: center;
  27. }
  28. main {
  29. flex: 1;
  30. display: flex;
  31. overflow: hidden;
  32. }
  33. #editor {
  34. width: 300px;
  35. background-color: #edf2f7;
  36. padding: 20px;
  37. overflow-y: auto;
  38. }
  39. #media-section, #slide-section, #audio-section {
  40. margin-bottom: 20px;
  41. }
  42. h2 {
  43. margin-top: 0;
  44. }
  45. #media-gallery, #slide-list {
  46. display: flex;
  47. flex-wrap: wrap;
  48. gap: 10px;
  49. }
  50. .media-item, .slide-item {
  51. width: 80px;
  52. height: 80px;
  53. background-size: cover;
  54. background-position: center;
  55. border: 2px solid #cbd5e0;
  56. cursor: pointer;
  57. position: relative;
  58. }
  59. .slide-item .remove-btn {
  60. position: absolute;
  61. top: -10px;
  62. right: -10px;
  63. background-color: #e53e3e;
  64. color: #fff;
  65. border: none;
  66. border-radius: 50%;
  67. width: 20px;
  68. height: 20px;
  69. cursor: pointer;
  70. }
  71. #preview {
  72. flex: 1;
  73. padding: 20px;
  74. display: flex;
  75. flex-direction: column;
  76. }
  77. #canvas {
  78. flex: 1;
  79. background-color: #000;
  80. }
  81. #controls {
  82. margin-top: 10px;
  83. text-align: center;
  84. }
  85. #controls button {
  86. padding: 10px 20px;
  87. margin: 0 5px;
  88. font-size: 16px;
  89. }
  90. </style>
  91. </head>
  92. <body>
  93. <div id="app">
  94. <header>
  95. <h1>MelindaAI AutoVideo - Advanced Video Creator</h1>
  96. </header>
  97. <main>
  98. <section id="editor">
  99. <div id="media-section">
  100. <h2>Media Library</h2>
  101. <input type="file" id="media-input" accept="image/*,video/*" multiple>
  102. <div id="media-gallery"></div>
  103. </div>
  104. <div id="slide-section">
  105. <h2>Slides</h2>
  106. <button id="new-slide-btn">Add Slide</button>
  107. <div id="slide-list"></div>
  108. </div>
  109. <div id="audio-section">
  110. <h2>Audio Settings</h2>
  111. <label>Background Music:</label>
  112. <input type="file" id="background-music-input" accept="audio/*"><br><br>
  113. <label>Voiceover:</label>
  114. <input type="file" id="voiceover-input" accept="audio/*">
  115. </div>
  116. </section>
  117. <section id="preview">
  118. <h2>Preview</h2>
  119. <canvas id="canvas"></canvas>
  120. <div id="controls">
  121. <button id="play-btn">Play</button>
  122. <button id="stop-btn" disabled>Stop</button>
  123. <button id="download-btn" disabled>Download Video</button>
  124. </div>
  125. </section>
  126. </main>
  127. </div>
  128. <script>
  129. document.addEventListener('DOMContentLoaded', () => {
  130. // Elements
  131. const mediaInput = document.getElementById('media-input');
  132. const mediaGallery = document.getElementById('media-gallery');
  133. const slideList = document.getElementById('slide-list');
  134. const newSlideBtn = document.getElementById('new-slide-btn');
  135. const playBtn = document.getElementById('play-btn');
  136. const stopBtn = document.getElementById('stop-btn');
  137. const downloadBtn = document.getElementById('download-btn');
  138. const canvas = document.getElementById('canvas');
  139. const ctx = canvas.getContext('2d');
  140. const canvasWidth = 1280;
  141. const canvasHeight = 720;
  142. canvas.width = canvasWidth;
  143. canvas.height = canvasHeight;
  144.  
  145. const backgroundMusicInput = document.getElementById('background-music-input');
  146. const voiceoverInput = document.getElementById('voiceover-input');
  147.  
  148. // Variables
  149. let mediaItems = [];
  150. let slides = [];
  151. let isPlaying = false;
  152. let currentSlideIndex = 0;
  153. let capturer = null;
  154. let recording = false;
  155. const frameRate = 30;
  156. let mediaRecorder;
  157. let recordedChunks = [];
  158. let backgroundMusic = null;
  159. let voiceover = null;
  160.  
  161. // Event Listeners
  162. mediaInput.addEventListener('change', handleMediaInput);
  163. newSlideBtn.addEventListener('click', () => addSlide(null));
  164. playBtn.addEventListener('click', startPresentation);
  165. stopBtn.addEventListener('click', stopPresentation);
  166. downloadBtn.addEventListener('click', downloadVideo);
  167.  
  168. backgroundMusicInput.addEventListener('change', handleBackgroundMusicInput);
  169. voiceoverInput.addEventListener('change', handleVoiceoverInput);
  170.  
  171. function handleMediaInput(event) {
  172. const files = event.target.files;
  173. for (let file of files) {
  174. const url = URL.createObjectURL(file);
  175. const mediaItem = {
  176. url,
  177. type: file.type.startsWith('video/') ? 'video' : 'image',
  178. };
  179. mediaItems.push(mediaItem);
  180. displayMediaItem(mediaItem);
  181. }
  182. }
  183.  
  184. function displayMediaItem(mediaItem) {
  185. const div = document.createElement('div');
  186. div.classList.add('media-item');
  187. div.style.backgroundImage = `url(${mediaItem.url})`;
  188. div.addEventListener('click', () => addSlide(mediaItem));
  189. mediaGallery.appendChild(div);
  190. }
  191.  
  192. function addSlide(mediaItem) {
  193. const slide = {
  194. mediaItem,
  195. duration: 5,
  196. title: 'Title Here',
  197. subtitle: 'Subtitle Here',
  198. kenBurns: true,
  199. };
  200. slides.push(slide);
  201. displaySlideItem(slide);
  202. }
  203.  
  204. function displaySlideItem(slide) {
  205. const div = document.createElement('div');
  206. div.classList.add('slide-item');
  207. if (slide.mediaItem) {
  208. div.style.backgroundImage = `url(${slide.mediaItem.url})`;
  209. } else {
  210. div.style.backgroundColor = '#cbd5e0';
  211. }
  212.  
  213. const removeBtn = document.createElement('button');
  214. removeBtn.classList.add('remove-btn');
  215. removeBtn.textContent = '×';
  216. removeBtn.addEventListener('click', (e) => {
  217. e.stopPropagation();
  218. removeSlide(slide);
  219. });
  220. div.appendChild(removeBtn);
  221.  
  222. div.addEventListener('click', () => editSlide(slide));
  223.  
  224. slideList.appendChild(div);
  225. }
  226.  
  227. function removeSlide(slide) {
  228. const index = slides.indexOf(slide);
  229. if (index > -1) {
  230. slides.splice(index, 1);
  231. renderSlideList();
  232. }
  233. }
  234.  
  235. function renderSlideList() {
  236. slideList.innerHTML = '';
  237. slides.forEach((slide) => displaySlideItem(slide));
  238. }
  239.  
  240. function editSlide(slide) {
  241. const title = prompt('Enter title:', slide.title);
  242. const subtitle = prompt('Enter subtitle:', slide.subtitle);
  243. const duration = prompt('Enter duration (seconds):', slide.duration);
  244. const kenBurns = confirm('Apply Ken Burns effect?');
  245.  
  246. if (title !== null) slide.title = title;
  247. if (subtitle !== null) slide.subtitle = subtitle;
  248. if (duration !== null) slide.duration = parseFloat(duration) || 5;
  249. slide.kenBurns = kenBurns;
  250. }
  251.  
  252. function handleBackgroundMusicInput(event) {
  253. const file = event.target.files[0];
  254. if (file) {
  255. const url = URL.createObjectURL(file);
  256. backgroundMusic = new Audio(url);
  257. backgroundMusic.loop = true;
  258. backgroundMusic.crossOrigin = 'anonymous';
  259. }
  260. }
  261.  
  262. function handleVoiceoverInput(event) {
  263. const file = event.target.files[0];
  264. if (file) {
  265. const url = URL.createObjectURL(file);
  266. voiceover = new Audio(url);
  267. voiceover.crossOrigin = 'anonymous';
  268. }
  269. }
  270.  
  271. function startPresentation() {
  272. if (isPlaying || slides.length === 0) return;
  273. isPlaying = true;
  274. playBtn.disabled = true;
  275. stopBtn.disabled = false;
  276. downloadBtn.disabled = true;
  277.  
  278. // Play audio
  279. if (backgroundMusic) backgroundMusic.play();
  280. if (voiceover) voiceover.play();
  281.  
  282. // Capture streams
  283. const canvasStream = canvas.captureStream(frameRate);
  284. const audioTracks = [];
  285.  
  286. if (backgroundMusic && backgroundMusic.captureStream) {
  287. const bgMusicStream = backgroundMusic.captureStream();
  288. audioTracks.push(...bgMusicStream.getAudioTracks());
  289. }
  290.  
  291. if (voiceover && voiceover.captureStream) {
  292. const voiceoverStream = voiceover.captureStream();
  293. audioTracks.push(...voiceoverStream.getAudioTracks());
  294. }
  295.  
  296. const combinedStream = new MediaStream([...canvasStream.getVideoTracks(), ...audioTracks]);
  297. mediaRecorder = new MediaRecorder(combinedStream, { mimeType: 'video/webm; codecs=vp9' });
  298. recordedChunks = [];
  299.  
  300. mediaRecorder.ondataavailable = (event) => {
  301. if (event.data.size > 0) {
  302. recordedChunks.push(event.data);
  303. }
  304. };
  305.  
  306. mediaRecorder.onstop = () => {
  307. const blob = new Blob(recordedChunks, { type: 'video/webm' });
  308. const url = URL.createObjectURL(blob);
  309. downloadBtn.href = url;
  310. downloadBtn.download = 'presentation.webm';
  311. downloadBtn.disabled = false;
  312. alert('Your video is ready to download.');
  313. };
  314.  
  315. mediaRecorder.start();
  316. recording = true;
  317.  
  318. currentSlideIndex = 0;
  319. renderFrame();
  320. }
  321.  
  322. function stopPresentation() {
  323. if (!isPlaying) return;
  324. isPlaying = false;
  325. playBtn.disabled = false;
  326. stopBtn.disabled = true;
  327.  
  328. if (backgroundMusic) backgroundMusic.pause();
  329. if (voiceover) voiceover.pause();
  330.  
  331. mediaRecorder.stop();
  332. recording = false;
  333. }
  334.  
  335. function renderFrame() {
  336. if (!isPlaying) return;
  337. const slide = slides[currentSlideIndex];
  338. const totalFrames = slide.duration * frameRate;
  339. const frameIndex = (slide.frameIndex || 0) + 1;
  340. slide.frameIndex = frameIndex;
  341.  
  342. ctx.clearRect(0, 0, canvasWidth, canvasHeight);
  343.  
  344. if (slide.mediaItem && slide.mediaItem.type === 'image') {
  345. renderImageSlide(slide, frameIndex / totalFrames);
  346. } else if (slide.mediaItem && slide.mediaItem.type === 'video') {
  347. renderVideoSlide(slide);
  348. } else {
  349. ctx.fillStyle = '#000';
  350. ctx.fillRect(0, 0, canvasWidth, canvasHeight);
  351. }
  352.  
  353. drawText(slide);
  354.  
  355. if (recording) {
  356. capturer.capture(canvas);
  357. }
  358.  
  359. if (frameIndex >= totalFrames) {
  360. slide.frameIndex = 0;
  361. currentSlideIndex++;
  362. if (currentSlideIndex >= slides.length) {
  363. stopPresentation();
  364. return;
  365. }
  366. }
  367.  
  368. requestAnimationFrame(renderFrame);
  369. }
  370.  
  371. function renderImageSlide(slide, progress) {
  372. const img = new Image();
  373. img.src = slide.mediaItem.url;
  374. img.onload = () => {
  375. let scale = 1;
  376. let x = 0;
  377. let y = 0;
  378. if (slide.kenBurns) {
  379. scale = 1 + 0.2 * progress;
  380. x = (canvasWidth - img.width * scale) / 2;
  381. y = (canvasHeight - img.height * scale) / 2;
  382. }
  383. ctx.drawImage(img, x, y, img.width * scale, img.height * scale);
  384. };
  385. }
  386.  
  387. function renderVideoSlide(slide) {
  388. if (!slide.videoElement) {
  389. const video = document.createElement('video');
  390. video.src = slide.mediaItem.url;
  391. video.crossOrigin = 'anonymous';
  392. video.load();
  393. video.play();
  394. slide.videoElement = video;
  395. }
  396. const video = slide.videoElement;
  397. ctx.drawImage(video, 0, 0, canvasWidth, canvasHeight);
  398. }
  399.  
  400. function drawText(slide) {
  401. ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
  402. ctx.fillRect(0, canvasHeight - 150, canvasWidth, 150);
  403. ctx.fillStyle = '#fff';
  404. ctx.font = 'bold 48px Arial';
  405. ctx.textAlign = 'center';
  406. ctx.fillText(slide.title, canvasWidth / 2, canvasHeight - 100);
  407. ctx.font = '32px Arial';
  408. ctx.fillText(slide.subtitle, canvasWidth / 2, canvasHeight - 50);
  409. }
  410.  
  411. function downloadVideo() {
  412. alert('Your video is ready for download.');
  413. }
  414. });
  415. </script>
  416. </body>
  417. </html>
Success #stdin #stdout 0.04s 25556KB
stdin
Standard input is empty
stdout
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>MelindaAI AutoVideo - Advanced Video Creator</title>
  <!-- Include GSAP from CDN -->
  <script src="https://c...content-available-to-author-only...e.com/ajax/libs/gsap/3.11.5/gsap.min.js"></script>
  <!-- Include CCapture.js from CDN -->
  <script src="https://c...content-available-to-author-only...r.net/npm/ccapture.js@1.1.10/build/CCapture.all.min.js"></script>
  <!-- Include ffmpeg.wasm from CDN -->
  <script src="https://c...content-available-to-author-only...r.net/npm/@ffmpeg/ffmpeg@0.11.5/dist/ffmpeg.min.js"></script>
  <style>
    body {
      margin: 0;
      font-family: Arial, sans-serif;
      background-color: #f9fafb;
    }
    #app {
      display: flex;
      flex-direction: column;
      height: 100vh;
    }
    header {
      background-color: #2d3748;
      color: #edf2f7;
      padding: 20px;
      text-align: center;
    }
    main {
      flex: 1;
      display: flex;
      overflow: hidden;
    }
    #editor {
      width: 300px;
      background-color: #edf2f7;
      padding: 20px;
      overflow-y: auto;
    }
    #media-section, #slide-section, #audio-section {
      margin-bottom: 20px;
    }
    h2 {
      margin-top: 0;
    }
    #media-gallery, #slide-list {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
    }
    .media-item, .slide-item {
      width: 80px;
      height: 80px;
      background-size: cover;
      background-position: center;
      border: 2px solid #cbd5e0;
      cursor: pointer;
      position: relative;
    }
    .slide-item .remove-btn {
      position: absolute;
      top: -10px;
      right: -10px;
      background-color: #e53e3e;
      color: #fff;
      border: none;
      border-radius: 50%;
      width: 20px;
      height: 20px;
      cursor: pointer;
    }
    #preview {
      flex: 1;
      padding: 20px;
      display: flex;
      flex-direction: column;
    }
    #canvas {
      flex: 1;
      background-color: #000;
    }
    #controls {
      margin-top: 10px;
      text-align: center;
    }
    #controls button {
      padding: 10px 20px;
      margin: 0 5px;
      font-size: 16px;
    }
  </style>
</head>
<body>
  <div id="app">
    <header>
      <h1>MelindaAI AutoVideo - Advanced Video Creator</h1>
    </header>
    <main>
      <section id="editor">
        <div id="media-section">
          <h2>Media Library</h2>
          <input type="file" id="media-input" accept="image/*,video/*" multiple>
          <div id="media-gallery"></div>
        </div>
        <div id="slide-section">
          <h2>Slides</h2>
          <button id="new-slide-btn">Add Slide</button>
          <div id="slide-list"></div>
        </div>
        <div id="audio-section">
          <h2>Audio Settings</h2>
          <label>Background Music:</label>
          <input type="file" id="background-music-input" accept="audio/*"><br><br>
          <label>Voiceover:</label>
          <input type="file" id="voiceover-input" accept="audio/*">
        </div>
      </section>
      <section id="preview">
        <h2>Preview</h2>
        <canvas id="canvas"></canvas>
        <div id="controls">
          <button id="play-btn">Play</button>
          <button id="stop-btn" disabled>Stop</button>
          <button id="download-btn" disabled>Download Video</button>
        </div>
      </section>
    </main>
  </div>
  <script>
    document.addEventListener('DOMContentLoaded', () => {
      // Elements
      const mediaInput = document.getElementById('media-input');
      const mediaGallery = document.getElementById('media-gallery');
      const slideList = document.getElementById('slide-list');
      const newSlideBtn = document.getElementById('new-slide-btn');
      const playBtn = document.getElementById('play-btn');
      const stopBtn = document.getElementById('stop-btn');
      const downloadBtn = document.getElementById('download-btn');
      const canvas = document.getElementById('canvas');
      const ctx = canvas.getContext('2d');
      const canvasWidth = 1280;
      const canvasHeight = 720;
      canvas.width = canvasWidth;
      canvas.height = canvasHeight;

      const backgroundMusicInput = document.getElementById('background-music-input');
      const voiceoverInput = document.getElementById('voiceover-input');

      // Variables
      let mediaItems = [];
      let slides = [];
      let isPlaying = false;
      let currentSlideIndex = 0;
      let capturer = null;
      let recording = false;
      const frameRate = 30;
      let mediaRecorder;
      let recordedChunks = [];
      let backgroundMusic = null;
      let voiceover = null;

      // Event Listeners
      mediaInput.addEventListener('change', handleMediaInput);
      newSlideBtn.addEventListener('click', () => addSlide(null));
      playBtn.addEventListener('click', startPresentation);
      stopBtn.addEventListener('click', stopPresentation);
      downloadBtn.addEventListener('click', downloadVideo);

      backgroundMusicInput.addEventListener('change', handleBackgroundMusicInput);
      voiceoverInput.addEventListener('change', handleVoiceoverInput);

      function handleMediaInput(event) {
        const files = event.target.files;
        for (let file of files) {
          const url = URL.createObjectURL(file);
          const mediaItem = {
            file,
            url,
            type: file.type.startsWith('video/') ? 'video' : 'image',
          };
          mediaItems.push(mediaItem);
          displayMediaItem(mediaItem);
        }
      }

      function displayMediaItem(mediaItem) {
        const div = document.createElement('div');
        div.classList.add('media-item');
        div.style.backgroundImage = `url(${mediaItem.url})`;
        div.addEventListener('click', () => addSlide(mediaItem));
        mediaGallery.appendChild(div);
      }

      function addSlide(mediaItem) {
        const slide = {
          mediaItem,
          duration: 5,
          title: 'Title Here',
          subtitle: 'Subtitle Here',
          kenBurns: true,
        };
        slides.push(slide);
        displaySlideItem(slide);
      }

      function displaySlideItem(slide) {
        const div = document.createElement('div');
        div.classList.add('slide-item');
        if (slide.mediaItem) {
          div.style.backgroundImage = `url(${slide.mediaItem.url})`;
        } else {
          div.style.backgroundColor = '#cbd5e0';
        }

        const removeBtn = document.createElement('button');
        removeBtn.classList.add('remove-btn');
        removeBtn.textContent = '×';
        removeBtn.addEventListener('click', (e) => {
          e.stopPropagation();
          removeSlide(slide);
        });
        div.appendChild(removeBtn);

        div.addEventListener('click', () => editSlide(slide));

        slideList.appendChild(div);
      }

      function removeSlide(slide) {
        const index = slides.indexOf(slide);
        if (index > -1) {
          slides.splice(index, 1);
          renderSlideList();
        }
      }

      function renderSlideList() {
        slideList.innerHTML = '';
        slides.forEach((slide) => displaySlideItem(slide));
      }

      function editSlide(slide) {
        const title = prompt('Enter title:', slide.title);
        const subtitle = prompt('Enter subtitle:', slide.subtitle);
        const duration = prompt('Enter duration (seconds):', slide.duration);
        const kenBurns = confirm('Apply Ken Burns effect?');

        if (title !== null) slide.title = title;
        if (subtitle !== null) slide.subtitle = subtitle;
        if (duration !== null) slide.duration = parseFloat(duration) || 5;
        slide.kenBurns = kenBurns;
      }

      function handleBackgroundMusicInput(event) {
        const file = event.target.files[0];
        if (file) {
          const url = URL.createObjectURL(file);
          backgroundMusic = new Audio(url);
          backgroundMusic.loop = true;
          backgroundMusic.crossOrigin = 'anonymous';
        }
      }

      function handleVoiceoverInput(event) {
        const file = event.target.files[0];
        if (file) {
          const url = URL.createObjectURL(file);
          voiceover = new Audio(url);
          voiceover.crossOrigin = 'anonymous';
        }
      }

      function startPresentation() {
        if (isPlaying || slides.length === 0) return;
        isPlaying = true;
        playBtn.disabled = true;
        stopBtn.disabled = false;
        downloadBtn.disabled = true;

        // Play audio
        if (backgroundMusic) backgroundMusic.play();
        if (voiceover) voiceover.play();

        // Capture streams
        const canvasStream = canvas.captureStream(frameRate);
        const audioTracks = [];

        if (backgroundMusic && backgroundMusic.captureStream) {
          const bgMusicStream = backgroundMusic.captureStream();
          audioTracks.push(...bgMusicStream.getAudioTracks());
        }

        if (voiceover && voiceover.captureStream) {
          const voiceoverStream = voiceover.captureStream();
          audioTracks.push(...voiceoverStream.getAudioTracks());
        }

        const combinedStream = new MediaStream([...canvasStream.getVideoTracks(), ...audioTracks]);
        mediaRecorder = new MediaRecorder(combinedStream, { mimeType: 'video/webm; codecs=vp9' });
        recordedChunks = [];

        mediaRecorder.ondataavailable = (event) => {
          if (event.data.size > 0) {
            recordedChunks.push(event.data);
          }
        };

        mediaRecorder.onstop = () => {
          const blob = new Blob(recordedChunks, { type: 'video/webm' });
          const url = URL.createObjectURL(blob);
          downloadBtn.href = url;
          downloadBtn.download = 'presentation.webm';
          downloadBtn.disabled = false;
          alert('Your video is ready to download.');
        };

        mediaRecorder.start();
        recording = true;

        currentSlideIndex = 0;
        renderFrame();
      }

      function stopPresentation() {
        if (!isPlaying) return;
        isPlaying = false;
        playBtn.disabled = false;
        stopBtn.disabled = true;

        if (backgroundMusic) backgroundMusic.pause();
        if (voiceover) voiceover.pause();

        mediaRecorder.stop();
        recording = false;
      }

      function renderFrame() {
        if (!isPlaying) return;
        const slide = slides[currentSlideIndex];
        const totalFrames = slide.duration * frameRate;
        const frameIndex = (slide.frameIndex || 0) + 1;
        slide.frameIndex = frameIndex;

        ctx.clearRect(0, 0, canvasWidth, canvasHeight);

        if (slide.mediaItem && slide.mediaItem.type === 'image') {
          renderImageSlide(slide, frameIndex / totalFrames);
        } else if (slide.mediaItem && slide.mediaItem.type === 'video') {
          renderVideoSlide(slide);
        } else {
          ctx.fillStyle = '#000';
          ctx.fillRect(0, 0, canvasWidth, canvasHeight);
        }

        drawText(slide);

        if (recording) {
          capturer.capture(canvas);
        }

        if (frameIndex >= totalFrames) {
          slide.frameIndex = 0;
          currentSlideIndex++;
          if (currentSlideIndex >= slides.length) {
            stopPresentation();
            return;
          }
        }

        requestAnimationFrame(renderFrame);
      }

      function renderImageSlide(slide, progress) {
        const img = new Image();
        img.src = slide.mediaItem.url;
        img.onload = () => {
          let scale = 1;
          let x = 0;
          let y = 0;
          if (slide.kenBurns) {
            scale = 1 + 0.2 * progress;
            x = (canvasWidth - img.width * scale) / 2;
            y = (canvasHeight - img.height * scale) / 2;
          }
          ctx.drawImage(img, x, y, img.width * scale, img.height * scale);
        };
      }

      function renderVideoSlide(slide) {
        if (!slide.videoElement) {
          const video = document.createElement('video');
          video.src = slide.mediaItem.url;
          video.crossOrigin = 'anonymous';
          video.load();
          video.play();
          slide.videoElement = video;
        }
        const video = slide.videoElement;
        ctx.drawImage(video, 0, 0, canvasWidth, canvasHeight);
      }

      function drawText(slide) {
        ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
        ctx.fillRect(0, canvasHeight - 150, canvasWidth, 150);
        ctx.fillStyle = '#fff';
        ctx.font = 'bold 48px Arial';
        ctx.textAlign = 'center';
        ctx.fillText(slide.title, canvasWidth / 2, canvasHeight - 100);
        ctx.font = '32px Arial';
        ctx.fillText(slide.subtitle, canvasWidth / 2, canvasHeight - 50);
      }

      function downloadVideo() {
        alert('Your video is ready for download.');
      }
    });
  </script>
</body>
</html>