eubottura commited on
Commit
f122893
·
verified ·
1 Parent(s): 73a2890

Manual changes saved

Browse files
Files changed (1) hide show
  1. script.js +832 -787
script.js CHANGED
@@ -55,6 +55,45 @@ class VideoEditorInteligente {
55
  formatErrors: new Map()
56
  };
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  this.init();
59
  }
60
 
@@ -63,6 +102,70 @@ class VideoEditorInteligente {
63
  this.setupAudienceSelection();
64
  this.setupAnalyzeButton();
65
  this.detectBrowserCapabilities();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  }
67
 
68
  // ✅ NOVO: Detecção de capacidades do navegador
@@ -217,7 +320,7 @@ class VideoEditorInteligente {
217
  this.log(`Validação concluída: ${validFiles} válidos, ${invalidFiles} inválidos`);
218
 
219
  if (invalidFiles > 0) {
220
- alert(`⚠️ Atenção: ${invalidFiles} arquivo(s) de vídeo inválido(s) detected. Verifique os formatos suportados: MP4, WebM, MOV, AVI`);
221
  }
222
  }
223
 
@@ -1263,6 +1366,7 @@ class VideoEditorInteligente {
1263
  };
1264
  }
1265
 
 
1266
  processAllTakes() {
1267
  if (!this.scriptBlocks || !this.analyzedTakes) return;
1268
 
@@ -1283,11 +1387,30 @@ class VideoEditorInteligente {
1283
  take.composition * 0.05
1284
  );
1285
 
1286
- take.approved = take.totalScore >= 75;
 
 
 
 
1287
  });
1288
  });
1289
  }
1290
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1291
  processVisualAnalysis() {
1292
  if (!this.analyzedTakes || this.analyzedTakes.length === 0) return;
1293
 
@@ -1338,18 +1461,40 @@ class VideoEditorInteligente {
1338
  if (currentGroup.length > 0) {
1339
  this.createContextualGroup(currentGroup, currentTheme, groupStartTime);
1340
  }
 
 
 
1341
  }
1342
 
 
1343
  createContextualGroup(blocks, theme, startTime) {
1344
- const bestTakes = this.analyzedTakes.flatMap(video =>
1345
- video.takes.filter(take =>
1346
- take.theme === theme &&
1347
- take.approved
1348
- ).sort((a, b) => b.totalScore - a.totalScore).slice(0, 3)
1349
- );
1350
-
1351
- const selectedTake = bestTakes.length > 0 ?
1352
- bestTakes[Math.floor(Math.random() * Math.min(bestTakes.length, 2))] : null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1353
 
1354
  const duration = blocks.reduce((sum, block) => sum + block.duration, 0);
1355
  const endTime = startTime + duration;
@@ -1363,13 +1508,118 @@ class VideoEditorInteligente {
1363
  startTime: startTime,
1364
  endTime: endTime,
1365
  duration: duration,
1366
- flowScore: selectedTake ? selectedTake.totalScore : 0,
1367
- bestTakes: bestTakes
 
 
 
 
 
 
 
1368
  };
1369
 
1370
  this.blockGroups.push(group);
1371
  }
1372
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1373
  generateIntelligentTimeline() {
1374
  if (!this.blockGroups || this.blockGroups.length === 0) return;
1375
 
@@ -1386,7 +1636,10 @@ class VideoEditorInteligente {
1386
  bestTakes: group.bestTakes,
1387
  flowScore: group.flowScore,
1388
  hasTake: !!group.take,
1389
- hasAudio: !!this.audioFile
 
 
 
1390
  }));
1391
 
1392
  this.analysisResults = {
@@ -1547,6 +1800,24 @@ class VideoEditorInteligente {
1547
  `;
1548
  }).join('');
1549
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1550
  groupElement.innerHTML = `
1551
  <div class="group-header">
1552
  <div class="group-info">
@@ -1568,6 +1839,7 @@ class VideoEditorInteligente {
1568
  </div>
1569
  </div>
1570
  </div>
 
1571
  ${blocksHtml}
1572
  ${group.take ? this.generateContextualTakeHtml(group) : this.generateMissingTakeHtml(group)}
1573
  `;
@@ -1578,15 +1850,36 @@ class VideoEditorInteligente {
1578
  feather.replace();
1579
  }
1580
 
 
1581
  generateContextualTakeHtml(group) {
1582
  const take = group.take;
 
 
 
 
 
 
 
 
 
 
 
 
1583
  return `
1584
- <div class="take-selection">
 
 
 
 
1585
  <div class="take-video-container">
1586
  <video class="take-video" muted>
1587
  <source src="${URL.createObjectURL(this.videoFiles[take.fileIndex])}" type="video/mp4">
1588
  </video>
1589
- <div class="take-overlay">${take.totalScore.toFixed(1)}/100</div>
 
 
 
 
1590
  </div>
1591
  <div class="take-details">
1592
  <h4><i data-feather="film"></i> ${take.fileName}</h4>
@@ -1613,7 +1906,7 @@ class VideoEditorInteligente {
1613
  </div>
1614
  <div class="metric-item">
1615
  <span class="metric-label">Score Total</span>
1616
- <span class="metric-value">${take.totalScore.toFixed(1)}/100</span>
1617
  </div>
1618
  <div class="metric-item">
1619
  <span class="metric-label">Orientação</span>
@@ -1765,96 +2058,246 @@ class VideoEditorInteligente {
1765
 
1766
  // ✅ IMPLEMENTAÇÃO COMPLETA: Sistema robusto de renderização
1767
  async createIntelligentVideo(timelineItems) {
1768
- if (this.isRendering && this.renderingPromise) {
1769
- this.log('Renderização já em andamento, aguardando...', 'warn');
1770
- try {
1771
- await this.renderingPromise;
1772
- } catch (error) {
1773
- this.log('Renderização anterior falhou, iniciando nova...', 'warn');
1774
- }
1775
  }
1776
 
1777
- this.renderingPromise = new Promise(async (resolve, reject) => {
1778
- if (this.isRendering) {
1779
- reject(new Error('Conflito: renderização iniciada simultaneamente'));
1780
- return;
1781
- }
 
 
 
 
 
 
 
 
 
 
 
1782
 
1783
- this.isRendering = true;
1784
- this.clearAllTimeouts();
1785
- this.retryAttempts.clear();
1786
 
1787
- // ✅ RESETAR ESTATÍSTICAS
1788
- this.renderingStats = {
1789
- totalFrames: 0,
1790
- successfulFrames: 0,
1791
- failedFrames: 0,
1792
- loadingFrames: 0,
1793
- averageFrameTime: 0,
1794
- codecErrors: new Map(),
1795
- formatErrors: new Map()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1796
  };
1797
 
1798
- try {
1799
- this.log(`Iniciando criação de vídeo inteligente (${this.videoOrientation} ${this.canvasDimensions.width}x${this.canvasDimensions.height})...`);
1800
 
1801
- const totalTime = timelineItems.reduce((sum, item) => sum + item.duration, 0);
1802
- const baseTimeout = 300000;
1803
- const perSecondTimeout = 3000;
1804
- const dynamicTimeout = Math.max(baseTimeout, totalTime * perSecondTimeout);
 
 
 
 
 
 
 
 
1805
 
1806
- this.renderingTimeout = setTimeout(() => {
1807
- this.log('Timeout geral de renderização alcançado', 'error');
1808
- this.cleanupRendering();
1809
- this.printRenderingStats();
1810
- reject(new Error('Timeout: renderização demorou demais'));
1811
- }, dynamicTimeout);
1812
 
1813
- this.updateExportProgress(30, 'Preparando mídias e pré-carregando vídeos...');
1814
- await this.preloadRequiredVideos(timelineItems);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1815
 
1816
- // CANVAS COM ORIENTAÇÃO E FORMATO DINÂMICOS
1817
- const canvas = document.createElement('canvas');
1818
- const ctx = canvas.getContext('2d');
1819
- canvas.width = this.canvasDimensions.width;
1820
- canvas.height = this.canvasDimensions.height;
1821
 
1822
- this.log(`Canvas criado: ${canvas.width}x${canvas.height}`);
 
1823
 
1824
- // DETECTAR MELHOR FORMATO DE SAÍDA
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1825
  const outputFormat = this.detectBestOutputFormat();
1826
- this.log(`Formato de saída escolhido: ${outputFormat.mimeType}`);
1827
-
1828
- const stream = canvas.captureStream(30);
1829
- const audioContext = new (window.AudioContext || window.webkitAudioContext)();
1830
-
1831
- this.log('Carregando áudio do narrador...');
1832
- const audioBuffer = await this.loadAudio(audioContext, this.audioFile);
1833
-
1834
- const gainNode = audioContext.createGain();
1835
- gainNode.gain.value = 0.7;
1836
- const audioDestination = audioContext.createMediaStreamDestination();
1837
- gainNode.connect(audioDestination);
1838
-
1839
- const audioSource = audioContext.createBufferSource();
1840
- audioSource.buffer = audioBuffer;
1841
- audioSource.connect(gainNode);
1842
 
1843
- const combinedStream = new MediaStream([
1844
- ...stream.getVideoTracks(),
1845
- ...audioDestination.stream.getAudioTracks()
1846
- ]);
 
 
 
 
 
 
 
 
1847
 
1848
- // CONFIGURAÇÃO OTIMIZADA DO MEDIA RECORDER
1849
- const mediaRecorderConfig = {
1850
  mimeType: outputFormat.mimeType,
1851
- videoBitsPerSecond: outputFormat.videoBitrate,
1852
- audioBitsPerSecond: 96000
1853
- };
1854
-
1855
- this.log(`Configurando MediaRecorder: ${JSON.stringify(mediaRecorderConfig)}`);
1856
-
1857
- const mediaRecorder = new MediaRecorder(combinedStream, mediaRecorderConfig);
1858
 
1859
  const chunks = [];
1860
  mediaRecorder.ondataavailable = (event) => {
@@ -1864,752 +2307,345 @@ class VideoEditorInteligente {
1864
  };
1865
 
1866
  mediaRecorder.onstop = () => {
1867
- this.log('Gravação finalizada, criando blob...');
1868
- this.cleanupRendering();
1869
- this.printRenderingStats();
1870
-
1871
  const blob = new Blob(chunks, { type: outputFormat.mimeType });
 
1872
  resolve(blob);
1873
  };
1874
 
1875
- mediaRecorder.onerror = (event) => {
1876
- this.log(`Erro no MediaRecorder: ${event.error}`, 'error');
1877
- this.cleanupRendering();
1878
- this.printRenderingStats();
1879
- reject(new Error('Erro ao gravar vídeo'));
1880
- };
1881
-
1882
- this.log('Iniciando gravação...');
1883
  mediaRecorder.start();
1884
- audioSource.start(0);
 
1885
 
1886
- this.updateExportProgress(50, 'Processando vídeos com sistema de retry...');
1887
- await this.processTimelineWithRetry(ctx, canvas, timelineItems, audioContext);
 
 
 
 
 
1888
 
1889
- setTimeout(() => {
1890
- if (this.isRendering) {
1891
- this.log('Finalizando gravação...');
 
 
 
 
 
 
 
1892
  mediaRecorder.stop();
 
1893
  }
1894
- }, 3000);
 
 
 
 
 
 
 
 
 
 
 
1895
 
1896
  } catch (error) {
1897
- this.cleanupRendering();
1898
- this.printRenderingStats();
1899
- this.log(`Erro na criação do vídeo: ${error.message}`, 'error');
1900
  reject(error);
1901
  }
1902
  });
1903
-
1904
- return this.renderingPromise;
1905
  }
1906
 
1907
- // ✅ NOVO: Detecção do melhor formato de saída
1908
- detectBestOutputFormat() {
1909
- const formats = [
1910
- {
1911
- mimeType: 'video/webm;codecs=vp9,opus',
1912
- extension: 'webm',
1913
- videoBitrate: 6000000,
1914
- priority: 1
1915
- },
1916
- {
1917
- mimeType: 'video/mp4;codecs=avc1.42E01E,mp4a.40.2',
1918
- extension: 'mp4',
1919
- videoBitrate: 8000000,
1920
- priority: 2
1921
- },
1922
- {
1923
- mimeType: 'video/webm;codecs=vp8,opus',
1924
- extension: 'webm',
1925
- videoBitrate: 4000000,
1926
- priority: 3
1927
- },
1928
- {
1929
- mimeType: 'video/webm;codecs=av1,opus',
1930
- extension: 'webm',
1931
- videoBitrate: 5000000,
1932
- priority: 4
1933
  }
1934
- ];
1935
-
1936
- // Testar suporte e escolher o melhor
1937
- for (const format of formats) {
1938
- if (MediaRecorder.isTypeSupported(format.mimeType)) {
1939
- this.log(`Formato suportado: ${format.mimeType} (prioridade: ${format.priority})`);
1940
- return format;
 
1941
  }
 
 
 
 
 
 
1942
  }
 
 
 
 
 
1943
 
1944
- // Fallback para o formato mais básico
1945
- const fallback = {
1946
- mimeType: 'video/webm',
1947
- extension: 'webm',
1948
- videoBitrate: 3000000,
1949
- priority: 999
1950
- };
 
 
 
 
 
 
 
1951
 
1952
- this.log(`Usando fallback: ${fallback.mimeType}`);
1953
- return fallback;
1954
  }
1955
 
1956
- // ✅ NOVO: Imprimir estatísticas de renderização
1957
- printRenderingStats() {
1958
- this.log('=== ESTATÍSTICAS DE RENDERIZAÇÃO ===');
1959
- this.log(`Frames totais: ${this.renderingStats.totalFrames}`);
1960
- this.log(`Frames bem-sucedidos: ${this.renderingStats.successfulFrames}`);
1961
- this.log(`Frames falhados: ${this.renderingStats.failedFrames}`);
1962
- this.log(`Frames de loading: ${this.renderingStats.loadingFrames}`);
1963
- this.log(`Tempo médio por frame: ${this.renderingStats.averageFrameTime.toFixed(2)}ms`);
1964
 
1965
- if (this.renderingStats.codecErrors.size > 0) {
1966
- this.log('Erros de codec:');
1967
- this.renderingStats.codecErrors.forEach((count, codec) => {
1968
- this.log(` ${codec}: ${count} erros`);
1969
- });
1970
  }
1971
 
1972
- if (this.renderingStats.formatErrors.size > 0) {
1973
- this.log('Erros de formato:');
1974
- this.renderingStats.formatErrors.forEach((error, file) => {
1975
- this.log(` ${file}: ${error}`);
1976
- });
1977
- }
1978
 
1979
- const successRate = this.renderingStats.totalFrames > 0
1980
- ? (this.renderingStats.successfulFrames / this.renderingStats.totalFrames * 100).toFixed(1)
1981
- : 0;
1982
 
1983
- this.log(`Taxa de sucesso: ${successRate}%`);
1984
- this.log('====================================');
1985
- }
1986
-
1987
- async preloadRequiredVideos(timelineItems) {
1988
- const requiredFileIndexes = new Set();
1989
 
1990
- timelineItems.forEach(item => {
1991
- if (item.hasTake && item.take) {
1992
- requiredFileIndexes.add(item.take.fileIndex);
1993
- }
1994
- });
 
 
 
 
 
 
1995
 
1996
- const preloadPromises = Array.from(requiredFileIndexes).map(async (fileIndex) => {
1997
- const file = this.videoFiles[fileIndex];
1998
- try {
1999
- await this.preloadVideo(file, fileIndex);
2000
- this.log(`Vídeo ${fileIndex} pré-carregado com sucesso`);
2001
- } catch (error) {
2002
- this.log(`Falha ao pré-carregar vídeo ${fileIndex}: ${error.message}`, 'warn');
2003
- this.renderingStats.formatErrors.set(file.name, error.message);
2004
  }
2005
- });
 
 
 
 
 
 
 
 
 
2006
 
2007
- await Promise.allSettled(preloadPromises);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2008
  }
2009
 
2010
- clearAllTimeouts() {
2011
- if (this.renderingTimeout) {
2012
- clearTimeout(this.renderingTimeout);
2013
- this.renderingTimeout = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2014
  }
2015
 
2016
- this.segmentTimeouts.forEach((timeoutId, segmentId) => {
2017
- clearTimeout(timeoutId);
2018
- });
2019
- this.segmentTimeouts.clear();
2020
 
2021
- this.log('Todos os timeouts limpos');
 
 
 
 
 
 
 
 
 
2022
  }
2023
 
2024
- cleanupRendering() {
2025
- this.isRendering = false;
2026
- this.renderingPromise = null;
2027
- this.clearAllTimeouts();
2028
- this.retryAttempts.clear();
2029
- this.log('Renderização limpa');
 
 
 
 
 
 
 
2030
  }
2031
 
2032
- // ✅ IMPLEMENTAÇÃO COMPLETA: Processamento com debugging extensivo
2033
- async processTimelineWithRetry(ctx, canvas, timelineItems, audioContext) {
2034
- return new Promise(async (resolve, reject) => {
 
 
 
2035
  try {
2036
- let currentTime = 0;
2037
- let successCount = 0;
2038
- let failureCount = 0;
2039
-
2040
- this.log(`Processando ${timelineItems.length} itens com sistema de retry e debugging`);
2041
-
2042
- for (let i = 0; i < timelineItems.length; i++) {
2043
- if (!this.isRendering) {
2044
- this.log('Renderização cancelada', 'warn');
2045
- resolve();
2046
- return;
2047
- }
2048
-
2049
- const item = timelineItems[i];
2050
-
2051
- this.log(`Processando item ${i + 1}/${timelineItems.length}: Grupo ${item.groupId} (${item.theme})`);
2052
-
2053
- if (!item.hasTake) {
2054
- this.log(`Item ${item.groupId} não tem take, pulando...`, 'warn');
2055
- currentTime += item.duration;
2056
- continue;
2057
- }
2058
-
2059
- let retryCount = 0;
2060
- const maxRetries = 3;
2061
- let success = false;
2062
- let finalFrameCount = 0;
2063
-
2064
- while (retryCount < maxRetries && !success) {
2065
- try {
2066
- const retryKey = `item-${item.groupId}`;
2067
-
2068
- if (retryCount > 0) {
2069
- this.log(`Retry ${retryCount}/${maxRetries} para item ${item.groupId}`);
2070
- await this.delay(3000 * retryCount);
2071
- }
2072
-
2073
- const baseTimeout = 90000; // Aumentado para 90s
2074
- const retryMultiplier = Math.pow(1.5, retryCount);
2075
- const segmentTimeoutMs = Math.max(baseTimeout * retryMultiplier, item.duration * 6000 + 20000);
2076
-
2077
- const frameCount = await this.renderVideoSegmentWithRetry(ctx, canvas, item, currentTime, audioContext, segmentTimeoutMs);
2078
-
2079
- finalFrameCount = frameCount;
2080
- success = true;
2081
- successCount++;
2082
- this.log(`Item ${item.groupId} processado com sucesso: ${frameCount} frames (tentativa ${retryCount + 1})`);
2083
-
2084
- } catch (segmentError) {
2085
- retryCount++;
2086
- this.log(`Tentativa ${retryCount} falhou para item ${item.groupId}: ${segmentError.message}`, 'error');
2087
-
2088
- // ✅ REGISTRAR ERROS DE CODEC/FORMATO
2089
- if (segmentError.message.includes('codec') || segmentError.message.includes('formato')) {
2090
- const codec = item.take?.codec || 'unknown';
2091
- const format = item.take?.format || 'unknown';
2092
- this.renderingStats.codecErrors.set(codec, (this.renderingStats.codecErrors.get(codec) || 0) + 1);
2093
- this.renderingStats.formatErrors.set(item.take.fileName, segmentError.message);
2094
- }
2095
-
2096
- if (retryCount >= maxRetries) {
2097
- failureCount++;
2098
- this.log(`Item ${item.groupId} falhou após ${maxRetries} tentativas, usando fallback`, 'error');
2099
-
2100
- const fallbackFrames = await this.renderAdvancedFallback(ctx, canvas, item, currentTime);
2101
- finalFrameCount = fallbackFrames;
2102
- success = true;
2103
- }
2104
- }
2105
- }
2106
-
2107
- this.log(`Item ${item.groupId}: ${finalFrameCount} frames renderizados`);
2108
- currentTime += item.duration;
2109
-
2110
- const pauseTime = Math.min(500, item.duration * 100);
2111
- await this.delay(pauseTime);
2112
- }
2113
-
2114
- this.log(`Processamento concluído: ${successCount} sucessos, ${failureCount} falhas com fallback`);
2115
- resolve();
2116
-
2117
  } catch (error) {
2118
- this.log(`Erro no processamento da timeline: ${error.message}`, 'error');
2119
- reject(error);
2120
  }
2121
- });
2122
- }
2123
-
2124
- // ✅ IMPLEMENTAÇÃO COMPLETA: Renderização com debugging detalhado
2125
- async renderVideoSegmentWithRetry(ctx, canvas, item, startTime, audioContext, segmentTimeoutMs) {
2126
- const segmentId = `segment-${item.groupId}`;
2127
 
2128
- return new Promise((resolve, reject) => {
2129
- const segmentTimeout = setTimeout(() => {
2130
- this.log(`Timeout no segmento ${item.groupId} após ${segmentTimeoutMs}ms`, 'error');
2131
- this.segmentTimeouts.delete(segmentId);
2132
- reject(new Error(`Timeout no segmento ${item.groupId}`));
2133
- }, segmentTimeoutMs);
2134
-
2135
- this.segmentTimeouts.set(segmentId, segmentTimeout);
2136
-
2137
  try {
2138
- // ✅ ESTRATÉGIA MELHORADA DE OBTENÇÃO DO VÍDEO
2139
- let video = this.videoPreloadCache.get(`video-${item.take.fileIndex}`);
2140
-
2141
- if (!video) {
2142
- const loadedVideo = this.loadedVideos[item.take.fileIndex];
2143
- video = loadedVideo ? loadedVideo.video : null;
2144
- }
2145
-
2146
- if (!video) {
2147
- video = document.createElement('video');
2148
- video.src = URL.createObjectURL(this.videoFiles[item.take.fileIndex]);
2149
- video.crossOrigin = 'anonymous';
2150
- }
2151
-
2152
- // Configuração robusta do vídeo
2153
- video.loop = false;
2154
- video.muted = true;
2155
- video.playsInline = true;
2156
- video.preload = 'auto';
2157
-
2158
- const targetDuration = item.duration * 1000;
2159
- const actualStartTime = Date.now();
2160
- let frameCount = 0;
2161
- let lastFrameTime = 0;
2162
- const targetFPS = 30;
2163
- const frameInterval = 1000 / targetFPS;
2164
-
2165
- let consecutiveErrors = 0;
2166
- const maxErrors = 30; // Aumentado para ser mais tolerante
2167
- let videoReadyStateReached = false;
2168
- let actualFramesRendered = 0;
2169
- let frameStartTime = 0;
2170
- const frameTimes = [];
2171
-
2172
- const checkVideoReady = () => {
2173
- return video.readyState >= 2 &&
2174
- video.videoWidth > 0 &&
2175
- video.videoHeight > 0 &&
2176
- !isNaN(video.duration) &&
2177
- video.duration > 0;
2178
- };
2179
-
2180
- const forceVideoLoad = async () => {
2181
- if (!checkVideoReady()) {
2182
- this.log(`Forçando carregamento do vídeo para item ${item.groupId}`);
2183
-
2184
- try {
2185
- video.load();
2186
- await this.delay(2000);
2187
-
2188
- video.currentTime = 0.1;
2189
- await this.waitForVideoSeek(video);
2190
- video.currentTime = 0;
2191
- await this.waitForVideoSeek(video);
2192
- } catch (seekError) {
2193
- this.log(`Erro ao forçar seek: ${seekError.message}`, 'warn');
2194
- }
2195
-
2196
- if (!checkVideoReady()) {
2197
- throw new Error(`Vídeo não está pronto após tentativas forçadas`);
2198
- }
2199
- }
2200
- };
2201
-
2202
- const renderFrame = async () => {
2203
- if (!this.isRendering) {
2204
- clearTimeout(segmentTimeout);
2205
- this.segmentTimeouts.delete(segmentId);
2206
- reject(new Error('Renderização cancelada'));
2207
- return;
2208
- }
2209
-
2210
- try {
2211
- const now = Date.now();
2212
- const elapsed = now - actualStartTime;
2213
- const progress = Math.min(1, elapsed / targetDuration);
2214
-
2215
- if (progress < 1 && consecutiveErrors < maxErrors) {
2216
- // Controle de FPS
2217
- if (now - lastFrameTime >= frameInterval) {
2218
- frameStartTime = Date.now();
2219
- frameCount++;
2220
- actualFramesRendered++;
2221
- lastFrameTime = now;
2222
- consecutiveErrors = 0;
2223
-
2224
- // ✅ ATUALIZAR ESTATÍSTICAS
2225
- this.renderingStats.totalFrames++;
2226
-
2227
- if (!videoReadyStateReached) {
2228
- if (checkVideoReady()) {
2229
- videoReadyStateReached = true;
2230
- this.log(`Vídeo pronto para item ${item.groupId}`);
2231
- this.renderingStats.successfulFrames++;
2232
- } else {
2233
- this.renderingStats.loadingFrames++;
2234
- try {
2235
- await forceVideoLoad();
2236
- } catch (forceError) {
2237
- this.log(`Falha ao forçar carregamento: ${forceError.message}`, 'error');
2238
- this.renderingStats.failedFrames++;
2239
- }
2240
- }
2241
- }
2242
-
2243
- const videoTime = item.take.startTime + (progress * item.duration);
2244
- const timeDiff = Math.abs(video.currentTime - videoTime);
2245
-
2246
- if (timeDiff > 0.5) {
2247
- try {
2248
- video.currentTime = videoTime;
2249
- } catch (timeError) {
2250
- this.log(`Erro ao ajustar tempo: ${timeError.message}`, 'warn');
2251
- }
2252
- }
2253
-
2254
- // Renderizar com tratamento robusto
2255
- try {
2256
- ctx.fillStyle = '#000000';
2257
- ctx.fillRect(0, 0, canvas.width, canvas.height);
2258
-
2259
- if (videoReadyStateReached && video.readyState >= 2) {
2260
- const drawSuccess = this.drawVideoSafely(ctx, video, canvas);
2261
- if (drawSuccess) {
2262
- this.drawOverlaySafely(ctx, item, progress, actualFramesRendered);
2263
- this.renderingStats.successfulFrames++;
2264
- } else {
2265
- this.drawLoadingScreen(ctx, item, 'Erro ao desenhar vídeo');
2266
- this.renderingStats.failedFrames++;
2267
- }
2268
- } else {
2269
- this.drawLoadingScreen(ctx, item, 'Carregando vídeo...');
2270
- this.renderingStats.loadingFrames++;
2271
- }
2272
-
2273
- } catch (drawError) {
2274
- consecutiveErrors++;
2275
- this.renderingStats.failedFrames++;
2276
- this.log(`Erro ao desenhar frame ${actualFramesRendered}: ${drawError.message}`, 'error');
2277
-
2278
- if (consecutiveErrors < maxErrors) {
2279
- this.drawErrorScreen(ctx, item, `Erro ${consecutiveErrors}/${maxErrors}`);
2280
- } else {
2281
- throw new Error(`Muitos erros consecutivos na renderização: ${drawError.message}`);
2282
- }
2283
- }
2284
-
2285
- // ✅ REGISTRAR TEMPO DO FRAME
2286
- const frameTime = Date.now() - frameStartTime;
2287
- frameTimes.push(frameTime);
2288
- if (frameTimes.length > 100) {
2289
- frameTimes.shift(); // Manter apenas últimos 100 frames
2290
- }
2291
- this.renderingStats.averageFrameTime = frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length;
2292
- }
2293
-
2294
- if (consecutiveErrors < maxErrors) {
2295
- requestAnimationFrame(renderFrame);
2296
- } else {
2297
- throw new Error('Muitos erros consecutivos na renderização');
2298
- }
2299
-
2300
- } else {
2301
- clearTimeout(segmentTimeout);
2302
- this.segmentTimeouts.delete(segmentId);
2303
-
2304
- if (consecutiveErrors >= maxErrors) {
2305
- reject(new Error(`Muitos erros consecutivos no segmento ${item.groupId}`));
2306
- } else {
2307
- this.log(`Segmento ${item.groupId} renderizado: ${actualFramesRendered} frames`);
2308
- resolve(actualFramesRendered);
2309
- }
2310
- }
2311
-
2312
- } catch (frameError) {
2313
- consecutiveErrors++;
2314
- this.renderingStats.failedFrames++;
2315
- this.log(`Erro no frame ${actualFramesRendered}: ${frameError.message}`, 'error');
2316
-
2317
- if (consecutiveErrors < maxErrors) {
2318
- requestAnimationFrame(renderFrame);
2319
- } else {
2320
- clearTimeout(segmentTimeout);
2321
- this.segmentTimeouts.delete(segmentId);
2322
- reject(new Error(`Erro crítico no segmento ${item.groupId}: ${frameError.message}`));
2323
- }
2324
- }
2325
- };
2326
-
2327
- const startRendering = async () => {
2328
- try {
2329
- await forceVideoLoad();
2330
-
2331
- video.currentTime = item.take.startTime;
2332
-
2333
- try {
2334
- await video.play();
2335
- } catch (playError) {
2336
- this.log(`Play falhou, continuando anyway: ${playError.message}`, 'warn');
2337
- }
2338
-
2339
- requestAnimationFrame(renderFrame);
2340
-
2341
- } catch (initError) {
2342
- clearTimeout(segmentTimeout);
2343
- this.segmentTimeouts.delete(segmentId);
2344
- reject(new Error(`Erro na inicialização: ${initError.message}`));
2345
- }
2346
- };
2347
-
2348
- if (checkVideoReady()) {
2349
- startRendering();
2350
- } else {
2351
- setTimeout(() => {
2352
- startRendering();
2353
- }, 3000); // Aumentado para 3s
2354
- }
2355
-
2356
  } catch (error) {
2357
- clearTimeout(segmentTimeout);
2358
- this.segmentTimeouts.delete(segmentId);
2359
- this.log(`Erro geral no segmento ${item.groupId}: ${error.message}`, 'error');
2360
- this.renderingStats.failedFrames++;
2361
- reject(error);
2362
  }
2363
- });
2364
- }
2365
-
2366
- async renderAdvancedFallback(ctx, canvas, item, currentTime) {
2367
- this.log(`Aplicando fallback avançado para item ${item.groupId}`);
2368
-
2369
- const duration = item.duration * 1000;
2370
- const frames = Math.floor(duration * 0.02); // Reduzir FPS para fallback
2371
- const frameDelay = duration / frames;
2372
- let actualFrames = 0;
2373
 
2374
- try {
2375
- const video = this.videoPreloadCache.get(`video-${item.take.fileIndex}`);
2376
- if (video && video.readyState >= 2) {
2377
- for (let i = 0; i < frames; i++) {
2378
- if (!this.isRendering) break;
2379
-
2380
- actualFrames++;
2381
- this.renderingStats.totalFrames++;
2382
-
2383
- const progress = i / frames;
2384
- const targetTime = item.take.startTime + (progress * item.duration);
2385
-
2386
- if (Math.abs(video.currentTime - targetTime) > 0.5) {
2387
- video.currentTime = targetTime;
2388
- await this.waitForVideoSeek(video);
2389
- }
2390
-
2391
- ctx.fillStyle = '#000000';
2392
- ctx.fillRect(0, 0, canvas.width, canvas.height);
2393
-
2394
- try {
2395
- const aspect = video.videoWidth / video.videoHeight;
2396
- const canvasAspect = canvas.width / canvas.height;
2397
-
2398
- let drawWidth, drawHeight, drawX, drawY;
2399
-
2400
- if (aspect > canvasAspect) {
2401
- drawHeight = canvas.height;
2402
- drawWidth = drawHeight * aspect;
2403
- drawX = (canvas.width - drawWidth) / 2;
2404
- drawY = 0;
2405
- } else {
2406
- drawWidth = canvas.width;
2407
- drawHeight = drawWidth / aspect;
2408
- drawX = 0;
2409
- drawY = (canvas.height - drawHeight) / 2;
2410
- }
2411
-
2412
- ctx.drawImage(video, drawX, drawY, drawWidth, drawHeight);
2413
- this.renderingStats.successfulFrames++;
2414
-
2415
- ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
2416
- ctx.fillRect(10, 10, 400, 100);
2417
- ctx.fillStyle = 'white';
2418
- ctx.font = 'bold 16px Arial';
2419
- ctx.fillText(`Grupo ${item.groupId} - ${item.theme}`, 20, 35);
2420
- ctx.font = '14px Arial';
2421
- ctx.fillText(`Fallback: Frame estático`, 20, 60);
2422
- ctx.fillText(`Progresso: ${(progress * 100).toFixed(1)}%`, 20, 85);
2423
- ctx.fillText(`Codec: ${item.take.codec || 'unknown'}`, 20, 105);
2424
-
2425
- } catch (drawError) {
2426
- this.log(`Erro no frame fallback ${i}: ${drawError.message}`, 'error');
2427
- this.renderingStats.failedFrames++;
2428
- }
2429
-
2430
- await this.delay(frameDelay);
2431
- }
2432
-
2433
- this.log(`Fallback renderizado: ${actualFrames} frames`);
2434
- return actualFrames;
2435
  }
2436
- } catch (error) {
2437
- this.log(`Fallback de frames falhou: ${error.message}`, 'warn');
2438
- }
2439
 
2440
- // Fallback final: tela informativa
2441
- for (let i = 0; i < frames; i++) {
2442
- if (!this.isRendering) break;
2443
-
2444
- actualFrames++;
2445
- this.renderingStats.totalFrames++;
2446
-
2447
- ctx.fillStyle = '#000000';
2448
- ctx.fillRect(0, 0, canvas.width, canvas.height);
2449
-
2450
- const progress = i / frames;
2451
-
2452
- ctx.fillStyle = '#333333';
2453
- ctx.fillRect(100, 100, canvas.width - 200, canvas.height - 200);
2454
-
2455
- ctx.fillStyle = 'white';
2456
- ctx.font = 'bold 24px Arial';
2457
- ctx.textAlign = 'center';
2458
- ctx.fillText(`Grupo ${item.groupId}`, canvas.width / 2, canvas.height / 2 - 60);
2459
-
2460
- ctx.font = '20px Arial';
2461
- ctx.fillText(`Tema: ${item.theme}`, canvas.width / 2, canvas.height / 2 - 20);
2462
-
2463
- ctx.font = '16px Arial';
2464
- ctx.fillText(`Ângulo: ${item.take.angle}`, canvas.width / 2, canvas.height / 2 + 20);
2465
- ctx.fillText(`Score: ${item.flowScore.toFixed(1)}%`, canvas.width / 2, canvas.height / 2 + 50);
2466
- ctx.fillText(`Modo: Informação Estática`, canvas.width / 2, canvas.height / 2 + 80);
2467
- ctx.fillText(`Codec: ${item.take.codec || 'unknown'}`, canvas.width / 2, canvas.height / 2 + 110);
2468
- ctx.fillText(`Formato: ${item.take.format || 'unknown'}`, canvas.width / 2, canvas.height / 2 + 140);
2469
-
2470
- const barWidth = 400;
2471
- const barHeight = 20;
2472
- const barX = (canvas.width - barWidth) / 2;
2473
- const barY = canvas.height / 2 + 180;
2474
-
2475
- ctx.fillStyle = '#555555';
2476
- ctx.fillRect(barX, barY, barWidth, barHeight);
2477
-
2478
- ctx.fillStyle = '#00ff00';
2479
- ctx.fillRect(barX, barY, barWidth * progress, barHeight);
2480
-
2481
- ctx.fillStyle = 'white';
2482
- ctx.font = '14px Arial';
2483
- ctx.fillText(`${(progress * 100).toFixed(1)}%`, canvas.width / 2, barY + 35);
2484
-
2485
- ctx.textAlign = 'left';
2486
- this.renderingStats.successfulFrames++;
2487
-
2488
- await this.delay(frameDelay);
2489
- }
2490
 
2491
- this.log(`Fallback informativo renderizado: ${actualFrames} frames`);
2492
- return actualFrames;
2493
  }
2494
 
2495
- drawVideoSafely(ctx, video, canvas) {
2496
- try {
2497
- if (!video.videoWidth || !video.videoHeight) {
2498
- return false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2499
  }
2500
-
2501
- const videoAspect = video.videoWidth / video.videoHeight;
2502
- const canvasAspect = canvas.width / canvas.height;
2503
-
2504
- let drawWidth, drawHeight, drawX, drawY;
2505
-
2506
- if (videoAspect > canvasAspect) {
2507
- drawHeight = canvas.height;
2508
- drawWidth = drawHeight * videoAspect;
2509
- drawX = (canvas.width - drawWidth) / 2;
2510
- drawY = 0;
2511
- } else {
2512
- drawWidth = canvas.width;
2513
- drawHeight = drawWidth / videoAspect;
2514
- drawX = 0;
2515
- drawY = (canvas.height - drawHeight) / 2;
2516
  }
2517
-
2518
- ctx.drawImage(video, drawX, drawY, drawWidth, drawHeight);
2519
-
2520
- return true;
2521
-
2522
- } catch (error) {
2523
- this.log(`Erro ao desenhar vídeo: ${error.message}`, 'error');
2524
- return false;
2525
- }
2526
- }
2527
-
2528
- drawOverlaySafely(ctx, item, progress, frameCount) {
2529
- try {
2530
- const alpha = Math.min(1, progress * 2);
2531
-
2532
- ctx.fillStyle = `rgba(0, 0, 0, ${0.6 * (1 - alpha * 0.3)})`;
2533
- ctx.fillRect(10, 10, 450, 100); // Aumentado para acomodar mais informações
2534
-
2535
- ctx.fillStyle = 'white';
2536
- ctx.font = 'bold 14px Arial';
2537
- ctx.fillText(`Grupo ${item.groupId} - ${item.theme}`, 20, 35);
2538
- ctx.font = '12px Arial';
2539
- ctx.fillText(`Score: ${item.flowScore.toFixed(1)}% | Frame: ${frameCount}`, 20, 55);
2540
- ctx.fillText(`Progresso: ${(progress * 100).toFixed(1)}% | ${this.videoOrientation}`, 20, 75);
2541
- ctx.fillText(`Codec: ${item.take.codec || 'unknown'} | Formato: ${item.take.format || 'unknown'}`, 20, 95);
2542
-
2543
- } catch (error) {
2544
- this.log(`Erro no overlay: ${error.message}`, 'error');
2545
- }
2546
- }
2547
-
2548
- drawErrorScreen(ctx, item, message) {
2549
- try {
2550
- ctx.fillStyle = '#000000';
2551
- ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
2552
-
2553
- ctx.fillStyle = '#ff4444';
2554
- ctx.font = 'bold 24px Arial';
2555
- ctx.textAlign = 'center';
2556
- ctx.fillText(`Erro no Grupo ${item.groupId}`, ctx.canvas.width / 2, ctx.canvas.height / 2 - 30);
2557
-
2558
- ctx.fillStyle = '#ffffff';
2559
- ctx.font = '16px Arial';
2560
- ctx.fillText(message, ctx.canvas.width / 2, ctx.canvas.height / 2 + 10);
2561
- ctx.fillText(`Codec: ${item.take.codec || 'unknown'}`, ctx.canvas.width / 2, ctx.canvas.height / 2 + 40);
2562
- ctx.fillText(`Formato: ${item.take.format || 'unknown'}`, ctx.canvas.width / 2, ctx.canvas.height / 2 + 70);
2563
-
2564
- ctx.textAlign = 'left';
2565
- } catch (error) {
2566
- this.log(`Erro ao desenhar tela de erro: ${error.message}`, 'error');
2567
- }
2568
- }
2569
-
2570
- drawLoadingScreen(ctx, item, message) {
2571
- try {
2572
- ctx.fillStyle = '#000000';
2573
- ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
2574
-
2575
- ctx.fillStyle = '#666666';
2576
- ctx.font = '18px Arial';
2577
- ctx.textAlign = 'center';
2578
- ctx.fillText(`${message}...`, ctx.canvas.width / 2, ctx.canvas.height / 2 - 20);
2579
-
2580
- ctx.font = '14px Arial';
2581
- ctx.fillText(`Grupo ${item.groupId} - ${item.theme}`, ctx.canvas.width / 2, ctx.canvas.height / 2 + 10);
2582
- ctx.fillText(`Codec: ${item.take.codec || 'unknown'}`, ctx.canvas.width / 2, ctx.canvas.height / 2 + 40);
2583
- ctx.fillText(`Formato: ${item.take.format || 'unknown'}`, ctx.canvas.width / 2, ctx.canvas.height / 2 + 70);
2584
-
2585
- ctx.textAlign = 'left';
2586
- } catch (error) {
2587
- this.log(`Erro ao desenhar tela de loading: ${error.message}`, 'error');
2588
  }
2589
- }
2590
-
2591
- async renderBlackScreen(ctx, canvas, duration) {
2592
- const frames = Math.floor(duration * 30);
2593
- const frameDelay = 1000 / 30;
2594
 
2595
- for (let i = 0; i < frames; i++) {
2596
- if (!this.isRendering) break;
2597
-
2598
- ctx.fillStyle = '#000000';
2599
- ctx.fillRect(0, 0, canvas.width, canvas.height);
2600
-
2601
- ctx.fillStyle = '#666666';
2602
- ctx.font = '16px Arial';
2603
- ctx.textAlign = 'center';
2604
- ctx.fillText(`Processando...`, canvas.width / 2, canvas.height / 2);
2605
-
2606
- await this.delay(frameDelay);
2607
- }
2608
- }
2609
-
2610
- async loadAudio(audioContext, file) {
2611
- const arrayBuffer = await file.arrayBuffer();
2612
- return await audioContext.decodeAudioData(arrayBuffer);
2613
  }
2614
 
2615
  async exportAudioOnly() {
@@ -2636,17 +2672,13 @@ class VideoEditorInteligente {
2636
  }, 2000);
2637
  }
2638
 
2639
- delay(ms) {
2640
- return new Promise(resolve => setTimeout(resolve, ms));
2641
- }
2642
-
2643
  exportProjectFile(isFallback = false) {
2644
  const projectData = {
2645
  metadata: {
2646
  projectName: `Projeto_Inteligente_${new Date().toISOString().slice(0, 10)}`,
2647
  audience: this.targetAudience,
2648
  createdAt: new Date().toISOString(),
2649
- version: '8.0 - Sistema Completo de Debugging e Compatibilidade',
2650
  totalBlocks: this.scriptBlocks ? this.scriptBlocks.length : 0,
2651
  totalGroups: this.blockGroups ? this.blockGroups.length : 0,
2652
  totalDuration: this.finalTimeline ?
@@ -2678,10 +2710,13 @@ class VideoEditorInteligente {
2678
  duration: group.take.duration,
2679
  orientation: group.take.orientation,
2680
  codec: group.take.codec,
2681
- format: group.take.format
 
 
2682
  } : null,
2683
  flowScore: group.flowScore || 0,
2684
- bestTakes: group.bestTakes || []
 
2685
  })),
2686
  timeline: this.finalTimeline || [],
2687
  qualityMetrics: this.qualityMetrics || {},
@@ -2704,7 +2739,12 @@ class VideoEditorInteligente {
2704
  formatValidation: true,
2705
  codecDetection: true,
2706
  debuggingSystem: true,
2707
- renderingStats: this.renderingStats
 
 
 
 
 
2708
  };
2709
 
2710
  const blob = new Blob([JSON.stringify(projectData, null, 2)], { type: 'application/json' });
@@ -2740,6 +2780,10 @@ class VideoEditorInteligente {
2740
  const modal = document.getElementById('exportProgress');
2741
  if (modal) modal.classList.remove('active');
2742
  }
 
 
 
 
2743
  }
2744
 
2745
  // Funções globais
@@ -2762,4 +2806,5 @@ function exportProjectFile() {
2762
  editor.exportProjectFile();
2763
  }
2764
 
 
2765
  const editor = new VideoEditorInteligente();
 
55
  formatErrors: new Map()
56
  };
57
 
58
+ // ✅ MELHORIAS CRÍTICAS - Sistema de renderização reprojetado
59
+ this.renderingState = {
60
+ isRendering: false,
61
+ currentSegment: null,
62
+ startTime: 0,
63
+ frameCount: 0,
64
+ droppedFrames: 0,
65
+ avgFrameTime: 0,
66
+ lastFrameTime: 0,
67
+ targetFPS: 30,
68
+ frameInterval: 1000 / 30
69
+ };
70
+
71
+ // ✅ Cache de vídeos com estado real-time
72
+ this.videoCache = new Map();
73
+ this.activeVideos = new Set();
74
+
75
+ // ✅ Sistema de sincronização robusto
76
+ this.syncManager = {
77
+ audioContext: null,
78
+ audioBuffer: null,
79
+ audioSource: null,
80
+ globalStartTime: 0,
81
+ audioStartTime: 0,
82
+ isAudioPlaying: false
83
+ };
84
+
85
+ // ✅ Fila de renderização com prioridade
86
+ this.renderQueue = [];
87
+ this.isProcessingQueue = false;
88
+
89
+ // ✅ Sistema de fallback instantâneo
90
+ this.fallbackSystem = {
91
+ isActive: false,
92
+ fallbackCanvas: null,
93
+ fallbackCtx: null,
94
+ lastGoodFrame: null
95
+ };
96
+
97
  this.init();
98
  }
99
 
 
102
  this.setupAudienceSelection();
103
  this.setupAnalyzeButton();
104
  this.detectBrowserCapabilities();
105
+ this.injectAdditionalStyles();
106
+ }
107
+
108
+ // ✅ NOVO: Injetar estilos adicionais para status de fallback
109
+ injectAdditionalStyles() {
110
+ const additionalStyles = `
111
+ .status-badge {
112
+ padding: 4px 8px;
113
+ border-radius: 4px;
114
+ font-size: 11px;
115
+ font-weight: bold;
116
+ margin-left: 8px;
117
+ }
118
+
119
+ .status-badge.approved {
120
+ background: #28a745;
121
+ color: white;
122
+ }
123
+
124
+ .status-badge.fallback {
125
+ background: #ffc107;
126
+ color: #212529;
127
+ }
128
+
129
+ .status-badge.generic {
130
+ background: #dc3545;
131
+ color: white;
132
+ }
133
+
134
+ .generic-take {
135
+ border: 2px dashed #dc3545;
136
+ background: #fff5f5;
137
+ }
138
+
139
+ .fallback-take {
140
+ border: 2px dashed #ffc107;
141
+ background: #fffdf5;
142
+ }
143
+
144
+ .low-score {
145
+ color: #dc3545 !important;
146
+ font-weight: bold;
147
+ }
148
+
149
+ .take-header {
150
+ display: flex;
151
+ align-items: center;
152
+ margin-bottom: 10px;
153
+ }
154
+
155
+ .theme-matching-warning {
156
+ background: #fff3cd;
157
+ border: 1px solid #ffeaa7;
158
+ border-radius: 4px;
159
+ padding: 8px 12px;
160
+ margin: 8px 0;
161
+ font-size: 12px;
162
+ color: #856404;
163
+ }
164
+ `;
165
+
166
+ const styleSheet = document.createElement('style');
167
+ styleSheet.textContent = additionalStyles;
168
+ document.head.appendChild(styleSheet);
169
  }
170
 
171
  // ✅ NOVO: Detecção de capacidades do navegador
 
320
  this.log(`Validação concluída: ${validFiles} válidos, ${invalidFiles} inválidos`);
321
 
322
  if (invalidFiles > 0) {
323
+ alert(`⚠️ Atenção: ${invalidFiles} arquivo(s) de vídeo inválido(s) detectados. Verifique os formatos suportados: MP4, WebM, MOV, AVI`);
324
  }
325
  }
326
 
 
1366
  };
1367
  }
1368
 
1369
+ // ✅ MELHORIA: Processar takes com limiares dinâmicos por tema
1370
  processAllTakes() {
1371
  if (!this.scriptBlocks || !this.analyzedTakes) return;
1372
 
 
1387
  take.composition * 0.05
1388
  );
1389
 
1390
+ // USAR LIMIAR DINÂMICO POR TEMA
1391
+ const threshold = this.calculateOptimalApprovalThreshold(take.theme);
1392
+ take.approved = take.totalScore >= threshold;
1393
+
1394
+ this.log(`Take ${take.theme}: score ${take.totalScore.toFixed(1)}, threshold ${threshold}, approved ${take.approved}`);
1395
  });
1396
  });
1397
  }
1398
 
1399
+ // ✅ NOVO: Calcular limiar ótimo de aprovação por tema
1400
+ calculateOptimalApprovalThreshold(theme) {
1401
+ const thresholds = {
1402
+ 'geral': 60, // Reduzido para tema genérico
1403
+ 'produto': 70,
1404
+ 'detalhe': 70,
1405
+ 'uso': 65,
1406
+ 'qualidade': 70,
1407
+ 'design': 65,
1408
+ 'conforto': 65
1409
+ };
1410
+
1411
+ return thresholds[theme] || 60; // Default mais baixo
1412
+ }
1413
+
1414
  processVisualAnalysis() {
1415
  if (!this.analyzedTakes || this.analyzedTakes.length === 0) return;
1416
 
 
1461
  if (currentGroup.length > 0) {
1462
  this.createContextualGroup(currentGroup, currentTheme, groupStartTime);
1463
  }
1464
+
1465
+ // ✅ VALIDAR COBERTURA DE TEMAS
1466
+ this.validateThemeCoverage();
1467
  }
1468
 
1469
+ // ✅ MELHORIA: Sistema de fallback automático para temas sem takes
1470
  createContextualGroup(blocks, theme, startTime) {
1471
+ // Buscar takes compatíveis com matching inteligente
1472
+ let bestTakes = this.findBestMatchingTakes(theme, this.analyzedTakes.flatMap(v => v.takes));
1473
+
1474
+ // Se não houver takes aprovados, usar os melhores disponíveis
1475
+ let approvedTakes = bestTakes.filter(take => take.approved);
1476
+ let selectedTake = null;
1477
+ let wasFallback = false;
1478
+ let wasGeneric = false;
1479
+
1480
+ if (approvedTakes.length > 0) {
1481
+ // Usar takes aprovados
1482
+ selectedTake = approvedTakes[Math.floor(Math.random() * Math.min(approvedTakes.length, 2))];
1483
+ } else if (bestTakes.length > 0) {
1484
+ // Fallback: usar melhor take disponível mesmo sem aprovação
1485
+ selectedTake = bestTakes[0];
1486
+ wasFallback = true;
1487
+ this.log(`Usando fallback não aprovado para tema "${theme}": score ${selectedTake.totalScore.toFixed(1)}`, 'warn');
1488
+
1489
+ // Aprovar temporariamente este take
1490
+ selectedTake.approved = true;
1491
+ selectedTake.wasFallback = true;
1492
+ } else {
1493
+ // Fallback final: criar take genérico
1494
+ selectedTake = this.createGenericTake(theme);
1495
+ wasGeneric = true;
1496
+ this.log(`Criando take genérico para tema "${theme}"`, 'warn');
1497
+ }
1498
 
1499
  const duration = blocks.reduce((sum, block) => sum + block.duration, 0);
1500
  const endTime = startTime + duration;
 
1508
  startTime: startTime,
1509
  endTime: endTime,
1510
  duration: duration,
1511
+ flowScore: selectedTake ? selectedTake.totalScore : 50,
1512
+ bestTakes: bestTakes.length > 0 ? bestTakes : [selectedTake],
1513
+ wasFallback: wasFallback,
1514
+ wasGeneric: wasGeneric,
1515
+ matchingInfo: {
1516
+ exactMatches: bestTakes.filter(t => t.theme === theme).length,
1517
+ similarMatches: wasFallback ? bestTakes.length : 0,
1518
+ isGeneric: wasGeneric
1519
+ }
1520
  };
1521
 
1522
  this.blockGroups.push(group);
1523
  }
1524
 
1525
+ // ✅ NOVO: Criar take genérico quando não há nenhum disponível
1526
+ createGenericTake(theme) {
1527
+ // Usar primeiro vídeo disponível como base
1528
+ const baseVideo = this.videoFiles[0];
1529
+ const baseIndex = 0;
1530
+
1531
+ return {
1532
+ fileIndex: baseIndex,
1533
+ fileName: baseVideo.name,
1534
+ theme: theme,
1535
+ duration: 4.0, // Duração padrão
1536
+ angle: 'Frontal', // Ângulo neutro
1537
+ quality: 70, // Qualidade média
1538
+ relevanceScore: 65, // Score mínimo
1539
+ totalScore: 65, // Score total mínimo
1540
+ type: 'generic',
1541
+ approved: true,
1542
+ wasGeneric: true,
1543
+ orientation: this.videoOrientation,
1544
+ codec: 'generic',
1545
+ format: 'generic',
1546
+ startTime: 0,
1547
+ endTime: 4.0
1548
+ };
1549
+ }
1550
+
1551
+ // ✅ NOVO: Melhorar matching de temas com similaridade
1552
+ findBestMatchingTakes(targetTheme, allTakes) {
1553
+ // Mapeamento de temas similares
1554
+ const similarThemes = {
1555
+ 'geral': ['produto', 'uso', 'qualidade', 'design'],
1556
+ 'produto': ['geral', 'qualidade', 'design'],
1557
+ 'uso': ['geral', 'produto', 'conforto'],
1558
+ 'qualidade': ['geral', 'produto', 'detalhe'],
1559
+ 'design': ['geral', 'produto', 'detalhe'],
1560
+ 'conforto': ['geral', 'uso', 'produto'],
1561
+ 'detalhe': ['geral', 'qualidade', 'produto']
1562
+ };
1563
+
1564
+ // Buscar exato primeiro
1565
+ let exactMatches = allTakes.filter(take => take.theme === targetTheme);
1566
+
1567
+ if (exactMatches.length > 0) {
1568
+ return exactMatches;
1569
+ }
1570
+
1571
+ // Buscar temas similares
1572
+ const similar = similarThemes[targetTheme] || [];
1573
+ let similarMatches = [];
1574
+
1575
+ for (const simTheme of similar) {
1576
+ const matches = allTakes.filter(take => take.theme === simTheme);
1577
+ if (matches.length > 0) {
1578
+ similarMatches = similarMatches.concat(matches);
1579
+ }
1580
+ }
1581
+
1582
+ if (similarMatches.length > 0) {
1583
+ this.log(`Usando temas similares para "${targetTheme}": ${similar.join(', ')}`, 'info');
1584
+ return similarMatches;
1585
+ }
1586
+
1587
+ // Retornar todos os takes ordenados por score
1588
+ return allTakes.sort((a, b) => b.totalScore - a.totalScore);
1589
+ }
1590
+
1591
+ // ✅ NOVO: Validar cobertura de temas
1592
+ validateThemeCoverage() {
1593
+ const themes = new Set();
1594
+ const themesWithTakes = new Set();
1595
+
1596
+ // Coletar todos os temas dos blocos
1597
+ this.scriptBlocks.forEach(block => {
1598
+ themes.add(block.theme);
1599
+ });
1600
+
1601
+ // Verificar quais temas têm takes
1602
+ themes.forEach(theme => {
1603
+ const hasTake = this.blockGroups.some(group =>
1604
+ group.theme === theme && group.take
1605
+ );
1606
+ if (hasTake) {
1607
+ themesWithTakes.add(theme);
1608
+ }
1609
+ });
1610
+
1611
+ // Reportar status
1612
+ const missingThemes = Array.from(themes).filter(t => !themesWithTakes.has(t));
1613
+
1614
+ if (missingThemes.length > 0) {
1615
+ this.log(`Temas sem takes: ${missingThemes.join(', ')}`, 'error');
1616
+ } else {
1617
+ this.log(`Todos os temas têm takes: ${Array.from(themes).join(', ')}`, 'success');
1618
+ }
1619
+
1620
+ return missingThemes.length === 0;
1621
+ }
1622
+
1623
  generateIntelligentTimeline() {
1624
  if (!this.blockGroups || this.blockGroups.length === 0) return;
1625
 
 
1636
  bestTakes: group.bestTakes,
1637
  flowScore: group.flowScore,
1638
  hasTake: !!group.take,
1639
+ hasAudio: !!this.audioFile,
1640
+ wasFallback: group.wasFallback || false,
1641
+ wasGeneric: group.wasGeneric || false,
1642
+ matchingInfo: group.matchingInfo || {}
1643
  }));
1644
 
1645
  this.analysisResults = {
 
1800
  `;
1801
  }).join('');
1802
 
1803
+ // ✅ MELHORIA: Adicionar aviso de matching se necessário
1804
+ let matchingWarning = '';
1805
+ if (group.matchingInfo && !group.matchingInfo.exactMatches && group.matchingInfo.similarMatches > 0) {
1806
+ matchingWarning = `
1807
+ <div class="theme-matching-warning">
1808
+ <i data-feather="info"></i>
1809
+ <strong>Matching de Temas:</strong> Usando ${group.matchingInfo.similarMatches} takes de temas similares (${group.matchingInfo.similarThemes ? group.matchingInfo.similarThemes.join(', ') : ''})
1810
+ </div>
1811
+ `;
1812
+ } else if (group.matchingInfo && group.matchingInfo.isGeneric) {
1813
+ matchingWarning = `
1814
+ <div class="theme-matching-warning">
1815
+ <i data-feather="alert-triangle"></i>
1816
+ <strong>Take Genérico:</strong> Nenhum take compatível encontrado, usando take gerado automaticamente
1817
+ </div>
1818
+ `;
1819
+ }
1820
+
1821
  groupElement.innerHTML = `
1822
  <div class="group-header">
1823
  <div class="group-info">
 
1839
  </div>
1840
  </div>
1841
  </div>
1842
+ ${matchingWarning}
1843
  ${blocksHtml}
1844
  ${group.take ? this.generateContextualTakeHtml(group) : this.generateMissingTakeHtml(group)}
1845
  `;
 
1850
  feather.replace();
1851
  }
1852
 
1853
+ // ✅ MELHORIA: Atualizar display para mostrar status de fallback
1854
  generateContextualTakeHtml(group) {
1855
  const take = group.take;
1856
+ const isFallback = group.wasFallback || false;
1857
+ const isGeneric = group.wasGeneric || false;
1858
+
1859
+ let statusBadge = '';
1860
+ if (isGeneric) {
1861
+ statusBadge = '<span class="status-badge generic">⚡ Genérico</span>';
1862
+ } else if (isFallback) {
1863
+ statusBadge = '<span class="status-badge fallback">🔄 Fallback</span>';
1864
+ } else {
1865
+ statusBadge = '<span class="status-badge approved">✅ Aprovado</span>';
1866
+ }
1867
+
1868
  return `
1869
+ <div class="take-selection ${isGeneric ? 'generic-take' : ''} ${isFallback ? 'fallback-take' : ''}">
1870
+ <div class="take-header">
1871
+ <h4><i data-feather="film"></i> ${take.fileName}</h4>
1872
+ ${statusBadge}
1873
+ </div>
1874
  <div class="take-video-container">
1875
  <video class="take-video" muted>
1876
  <source src="${URL.createObjectURL(this.videoFiles[take.fileIndex])}" type="video/mp4">
1877
  </video>
1878
+ <div class="take-overlay">
1879
+ Score: ${take.totalScore.toFixed(1)}/100
1880
+ ${take.wasGeneric ? '<br><small>Gerado automaticamente</small>' : ''}
1881
+ ${take.wasFallback ? '<br><small>Fallback não aprovado</small>' : ''}
1882
+ </div>
1883
  </div>
1884
  <div class="take-details">
1885
  <h4><i data-feather="film"></i> ${take.fileName}</h4>
 
1906
  </div>
1907
  <div class="metric-item">
1908
  <span class="metric-label">Score Total</span>
1909
+ <span class="metric-value ${take.totalScore < 70 ? 'low-score' : ''}">${take.totalScore.toFixed(1)}/100</span>
1910
  </div>
1911
  <div class="metric-item">
1912
  <span class="metric-label">Orientação</span>
 
2058
 
2059
  // ✅ IMPLEMENTAÇÃO COMPLETA: Sistema robusto de renderização
2060
  async createIntelligentVideo(timelineItems) {
2061
+ if (this.renderingState.isRendering) {
2062
+ throw new Error('Renderização já em andamento');
 
 
 
 
 
2063
  }
2064
 
2065
+ this.renderingState.isRendering = true;
2066
+ this.renderingState.startTime = Date.now();
2067
+ this.renderingState.frameCount = 0;
2068
+ this.renderingState.droppedFrames = 0;
2069
+
2070
+ try {
2071
+ this.log(`Iniciando renderização inteligente: ${timelineItems.length} itens`);
2072
+
2073
+ // ✅ ETAPA 1: Pré-carregar todos os vídeos necessários
2074
+ await this.preloadAllRequiredVideos(timelineItems);
2075
+
2076
+ // ✅ ETAPA 2: Configurar sincronização de áudio
2077
+ await this.setupAudioSync();
2078
+
2079
+ // ✅ ETAPA 3: Criar canvas de renderização
2080
+ const { canvas, ctx } = this.createRenderCanvas();
2081
 
2082
+ // ETAPA 4: Iniciar renderização em tempo real
2083
+ const videoBlob = await this.renderRealtime(ctx, canvas, timelineItems);
 
2084
 
2085
+ return videoBlob;
2086
+
2087
+ } finally {
2088
+ this.cleanupRendering();
2089
+ }
2090
+ }
2091
+
2092
+ // MÉTODO ESSENCIAL - Sistema robusto de carregamento de vídeo
2093
+ async loadVideoRobust(fileIndex) {
2094
+ const cacheKey = `video-${fileIndex}`;
2095
+
2096
+ if (this.videoCache.has(cacheKey)) {
2097
+ const cached = this.videoCache.get(cacheKey);
2098
+ if (cached.isReady && cached.video.readyState >= 4) {
2099
+ this.log(`Usando vídeo em cache: ${fileIndex}`);
2100
+ return cached;
2101
+ }
2102
+ }
2103
+
2104
+ this.log(`Carregando vídeo robusto: ${fileIndex}`);
2105
+
2106
+ const video = document.createElement('video');
2107
+ const file = this.videoFiles[fileIndex];
2108
+
2109
+ return new Promise((resolve, reject) => {
2110
+ let loadTimeout = null;
2111
+ let metadataTimeout = null;
2112
+ let seekTimeout = null;
2113
+
2114
+ const cleanup = () => {
2115
+ if (loadTimeout) clearTimeout(loadTimeout);
2116
+ if (metadataTimeout) clearTimeout(metadataTimeout);
2117
+ if (seekTimeout) clearTimeout(seekTimeout);
2118
+ this.activeVideos.delete(video);
2119
  };
2120
 
2121
+ const handleSuccess = () => {
2122
+ cleanup();
2123
 
2124
+ const videoData = {
2125
+ video: video,
2126
+ isReady: true,
2127
+ fileIndex: fileIndex,
2128
+ fileName: file.name,
2129
+ duration: video.duration,
2130
+ width: video.videoWidth,
2131
+ height: video.videoHeight,
2132
+ aspectRatio: video.videoWidth / video.videoHeight,
2133
+ codec: this.detectVideoCodec(file),
2134
+ loadTime: Date.now()
2135
+ };
2136
 
2137
+ this.videoCache.set(cacheKey, videoData);
2138
+ this.activeVideos.add(video);
 
 
 
 
2139
 
2140
+ this.log(`Vídeo carregado com sucesso: ${file.name} (${videoData.width}x${videoData.height})`);
2141
+ resolve(videoData);
2142
+ };
2143
+
2144
+ const handleError = (error) => {
2145
+ cleanup();
2146
+ URL.revokeObjectURL(video.src);
2147
+ this.log(`Erro ao carregar vídeo ${fileIndex}: ${error.message}`, 'error');
2148
+ reject(error);
2149
+ };
2150
+
2151
+ // Configurar vídeo com opções otimizadas
2152
+ video.src = URL.createObjectURL(file);
2153
+ video.crossOrigin = 'anonymous';
2154
+ video.preload = 'auto';
2155
+ video.muted = true;
2156
+ video.loop = false;
2157
+ video.playsInline = true;
2158
+
2159
+ // Timeout total de carregamento (30 segundos)
2160
+ loadTimeout = setTimeout(() => {
2161
+ handleError(new Error(`Timeout no carregamento do vídeo ${fileIndex}`));
2162
+ }, 30000);
2163
+
2164
+ // Verificar metadata rapidamente
2165
+ metadataTimeout = setTimeout(() => {
2166
+ if (video.readyState < 1) {
2167
+ this.log(`Forçando carregamento para vídeo ${fileIndex}`, 'warn');
2168
+ video.load();
2169
+ }
2170
+ }, 2000);
2171
+
2172
+ video.addEventListener('loadedmetadata', () => {
2173
+ if (metadataTimeout) {
2174
+ clearTimeout(metadataTimeout);
2175
+ metadataTimeout = null;
2176
+ }
2177
 
2178
+ if (!video.videoWidth || !video.videoHeight || !video.duration) {
2179
+ handleError(new Error(`Metadata inválida: ${video.videoWidth}x${video.videoHeight}, dur: ${video.duration}`));
2180
+ return;
2181
+ }
 
2182
 
2183
+ // Pré-carregar primeiro frame
2184
+ video.currentTime = 0.1;
2185
 
2186
+ seekTimeout = setTimeout(() => {
2187
+ // Mesmo se o seek falhar, continuamos se tivermos metadata válida
2188
+ if (video.readyState >= 2) {
2189
+ video.currentTime = 0;
2190
+ handleSuccess();
2191
+ } else {
2192
+ handleError(new Error(`ReadyState insuficiente: ${video.readyState}`));
2193
+ }
2194
+ }, 5000);
2195
+ }, { once: true });
2196
+
2197
+ video.addEventListener('seeked', () => {
2198
+ if (seekTimeout && video.currentTime === 0.1) {
2199
+ clearTimeout(seekTimeout);
2200
+ seekTimeout = null;
2201
+ video.currentTime = 0;
2202
+ handleSuccess();
2203
+ }
2204
+ }, { once: true });
2205
+
2206
+ video.addEventListener('error', (e) => {
2207
+ handleError(new Error(`Erro no vídeo: ${e.message || 'Erro desconhecido'}`));
2208
+ }, { once: true });
2209
+
2210
+ // Iniciar carregamento
2211
+ video.load();
2212
+ });
2213
+ }
2214
+
2215
+ // ✅ ETAPA 1: Pré-carregamento paralelo otimizado
2216
+ async preloadAllRequiredVideos(timelineItems) {
2217
+ const requiredIndexes = new Set();
2218
+ timelineItems.forEach(item => {
2219
+ if (item.hasTake && item.take) {
2220
+ requiredIndexes.add(item.take.fileIndex);
2221
+ }
2222
+ });
2223
+
2224
+ this.log(`Pré-carregando ${requiredIndexes.size} vídeos...`);
2225
+
2226
+ const loadPromises = Array.from(requiredIndexes).map(async (index) => {
2227
+ try {
2228
+ await this.loadVideoRobust(index);
2229
+ this.log(`Vídeo ${index} pré-carregado`);
2230
+ } catch (error) {
2231
+ this.log(`Falha no pré-carregamento do vídeo ${index}: ${error.message}`, 'warn');
2232
+ // Continuar mesmo com falha - sistema de fallback lidará com isso
2233
+ }
2234
+ });
2235
+
2236
+ await Promise.allSettled(loadPromises);
2237
+ this.log('Pré-carregamento concluído');
2238
+ }
2239
+
2240
+ // ✅ ETAPA 2: Configurar sincronização de áudio robusta
2241
+ async setupAudioSync() {
2242
+ this.syncManager.audioContext = new (window.AudioContext || window.webkitAudioContext)();
2243
+
2244
+ try {
2245
+ const arrayBuffer = await this.audioFile.arrayBuffer();
2246
+ this.syncManager.audioBuffer = await this.syncManager.audioContext.decodeAudioData(arrayBuffer);
2247
+ this.log(`Áudio carregado: ${this.syncManager.audioBuffer.duration.toFixed(1)}s`);
2248
+ } catch (error) {
2249
+ this.log(`Erro ao carregar áudio: ${error.message}`, 'warn');
2250
+ // Continuar sem áudio se falhar
2251
+ }
2252
+ }
2253
+
2254
+ // ✅ ETAPA 3: Canvas de renderização otimizado
2255
+ createRenderCanvas() {
2256
+ const canvas = document.createElement('canvas');
2257
+ canvas.width = this.canvasDimensions.width;
2258
+ canvas.height = this.canvasDimensions.height;
2259
+
2260
+ // Configurar contexto para melhor performance
2261
+ const ctx = canvas.getContext('2d', {
2262
+ alpha: false,
2263
+ desynchronized: true,
2264
+ willReadFrequently: false
2265
+ });
2266
+
2267
+ // Configurar image smoothing para melhor qualidade
2268
+ ctx.imageSmoothingEnabled = true;
2269
+ ctx.imageSmoothingQuality = 'high';
2270
+
2271
+ this.log(`Canvas criado: ${canvas.width}x${canvas.height}`);
2272
+
2273
+ return { canvas, ctx };
2274
+ }
2275
+
2276
+ // ✅ ETAPA 4: Renderização em tempo real com controle preciso
2277
+ async renderRealtime(ctx, canvas, timelineItems) {
2278
+ return new Promise(async (resolve, reject) => {
2279
+ try {
2280
+ const totalTime = timelineItems.reduce((sum, item) => sum + item.duration, 0);
2281
  const outputFormat = this.detectBestOutputFormat();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2282
 
2283
+ // Configurar stream de vídeo
2284
+ const stream = canvas.captureStream(this.renderingState.targetFPS);
2285
+
2286
+ // Configurar áudio se disponível
2287
+ let combinedStream = stream;
2288
+ if (this.syncManager.audioBuffer) {
2289
+ const audioStream = await this.createAudioStream();
2290
+ combinedStream = new MediaStream([
2291
+ ...stream.getVideoTracks(),
2292
+ ...audioStream.getAudioTracks()
2293
+ ]);
2294
+ }
2295
 
2296
+ // Configurar MediaRecorder
2297
+ const mediaRecorder = new MediaRecorder(combinedStream, {
2298
  mimeType: outputFormat.mimeType,
2299
+ videoBitsPerSecond: outputFormat.videoBitrate
2300
+ });
 
 
 
 
 
2301
 
2302
  const chunks = [];
2303
  mediaRecorder.ondataavailable = (event) => {
 
2307
  };
2308
 
2309
  mediaRecorder.onstop = () => {
 
 
 
 
2310
  const blob = new Blob(chunks, { type: outputFormat.mimeType });
2311
+ this.log(`Renderização concluída: ${blob.size} bytes`);
2312
  resolve(blob);
2313
  };
2314
 
2315
+ // Iniciar gravação
 
 
 
 
 
 
 
2316
  mediaRecorder.start();
2317
+ this.syncManager.globalStartTime = Date.now();
2318
+ this.syncManager.audioStartTime = this.syncManager.globalStartTime;
2319
 
2320
+ // Iniciar áudio se disponível
2321
+ if (this.syncManager.audioBuffer) {
2322
+ this.startAudioPlayback();
2323
+ }
2324
+
2325
+ // Loop de renderização principal
2326
+ this.renderingState.lastFrameTime = performance.now();
2327
 
2328
+ const renderLoop = () => {
2329
+ if (!this.renderingState.isRendering) {
2330
+ mediaRecorder.stop();
2331
+ return;
2332
+ }
2333
+
2334
+ const now = performance.now();
2335
+ const elapsed = (now - this.syncManager.globalStartTime) / 1000; // Convert to seconds
2336
+
2337
+ if (elapsed >= totalTime) {
2338
  mediaRecorder.stop();
2339
+ return;
2340
  }
2341
+
2342
+ // Controlar FPS
2343
+ const deltaTime = now - this.renderingState.lastFrameTime;
2344
+ if (deltaTime >= this.renderingState.frameInterval) {
2345
+ this.renderFrame(ctx, canvas, timelineItems, elapsed);
2346
+ this.renderingState.lastFrameTime = now - (deltaTime % this.renderingState.frameInterval);
2347
+ }
2348
+
2349
+ requestAnimationFrame(renderLoop);
2350
+ };
2351
+
2352
+ requestAnimationFrame(renderLoop);
2353
 
2354
  } catch (error) {
 
 
 
2355
  reject(error);
2356
  }
2357
  });
 
 
2358
  }
2359
 
2360
+ // ✅ FRAME POR FRAME - Renderização precisa com fallback
2361
+ renderFrame(ctx, canvas, timelineItems, currentTime) {
2362
+ try {
2363
+ // Encontrar o segmento atual
2364
+ const currentSegment = this.findCurrentSegment(timelineItems, currentTime);
2365
+
2366
+ if (!currentSegment) {
2367
+ this.renderBlackScreen(ctx, canvas);
2368
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2369
  }
2370
+
2371
+ // Tentar renderizar o vídeo
2372
+ const videoData = this.videoCache.get(`video-${currentSegment.take.fileIndex}`);
2373
+
2374
+ if (videoData && videoData.isReady) {
2375
+ this.renderVideoFrame(ctx, canvas, videoData.video, currentSegment, currentTime);
2376
+ } else {
2377
+ this.renderFallbackScreen(ctx, canvas, currentSegment, currentTime);
2378
  }
2379
+
2380
+ this.renderingState.frameCount++;
2381
+
2382
+ } catch (error) {
2383
+ this.log(`Erro no frame ${this.renderingState.frameCount}: ${error.message}`, 'error');
2384
+ this.renderingState.droppedFrames++;
2385
  }
2386
+ }
2387
+
2388
+ // ✅ ENCONTRAR SEGMENTO ATUAL - Algoritmo otimizado
2389
+ findCurrentSegment(timelineItems, currentTime) {
2390
+ let accumulatedTime = 0;
2391
 
2392
+ for (const item of timelineItems) {
2393
+ if (!item.hasTake) continue;
2394
+
2395
+ if (currentTime >= accumulatedTime && currentTime < accumulatedTime + item.duration) {
2396
+ const localTime = currentTime - accumulatedTime;
2397
+ return {
2398
+ ...item,
2399
+ localTime: localTime,
2400
+ videoTime: item.take.startTime + localTime
2401
+ };
2402
+ }
2403
+
2404
+ accumulatedTime += item.duration;
2405
+ }
2406
 
2407
+ return null;
 
2408
  }
2409
 
2410
+ // ✅ RENDERIZAR VÍDEO - Sincronização precisa
2411
+ renderVideoFrame(ctx, canvas, video, segment, currentTime) {
2412
+ // Sincronizar tempo do vídeo
2413
+ const targetTime = segment.videoTime;
2414
+ const currentTimeDiff = Math.abs(video.currentTime - targetTime);
 
 
 
2415
 
2416
+ if (currentTimeDiff > 0.1) {
2417
+ video.currentTime = targetTime;
 
 
 
2418
  }
2419
 
2420
+ // Limpar canvas
2421
+ ctx.fillStyle = '#000000';
2422
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
 
 
 
2423
 
2424
+ // Calcular dimensões para manter aspect ratio
2425
+ const videoAspect = video.videoWidth / video.videoHeight;
2426
+ const canvasAspect = canvas.width / canvas.height;
2427
 
2428
+ let drawWidth, drawHeight, drawX, drawY;
 
 
 
 
 
2429
 
2430
+ if (videoAspect > canvasAspect) {
2431
+ drawHeight = canvas.height;
2432
+ drawWidth = drawHeight * videoAspect;
2433
+ drawX = (canvas.width - drawWidth) / 2;
2434
+ drawY = 0;
2435
+ } else {
2436
+ drawWidth = canvas.width;
2437
+ drawHeight = drawWidth / videoAspect;
2438
+ drawX = 0;
2439
+ drawY = (canvas.height - drawHeight) / 2;
2440
+ }
2441
 
2442
+ // Desenhar vídeo
2443
+ try {
2444
+ ctx.drawImage(video, drawX, drawY, drawWidth, drawHeight);
2445
+
2446
+ // Overlay de informações (opcional)
2447
+ if (this.debugMode) {
2448
+ this.renderDebugOverlay(ctx, segment, currentTime);
 
2449
  }
2450
+ } catch (error) {
2451
+ this.log(`Erro ao desenhar vídeo: ${error.message}`, 'warn');
2452
+ this.renderFallbackScreen(ctx, canvas, segment, currentTime);
2453
+ }
2454
+ }
2455
+
2456
+ // ✅ TELA PRETA - Para gaps na timeline
2457
+ renderBlackScreen(ctx, canvas) {
2458
+ ctx.fillStyle = '#000000';
2459
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
2460
 
2461
+ if (this.debugMode) {
2462
+ ctx.fillStyle = '#666666';
2463
+ ctx.font = '16px Arial';
2464
+ ctx.textAlign = 'center';
2465
+ ctx.fillText('Aguardando próximo segmento...', canvas.width / 2, canvas.height / 2);
2466
+ ctx.textAlign = 'left';
2467
+ }
2468
+ }
2469
+
2470
+ // ✅ TELA DE FALLBACK - Quando o vídeo não está disponível
2471
+ renderFallbackScreen(ctx, canvas, segment, currentTime) {
2472
+ ctx.fillStyle = '#000000';
2473
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
2474
+
2475
+ ctx.fillStyle = '#333333';
2476
+ ctx.fillRect(50, 50, canvas.width - 100, canvas.height - 100);
2477
+
2478
+ ctx.fillStyle = 'white';
2479
+ ctx.font = 'bold 24px Arial';
2480
+ ctx.textAlign = 'center';
2481
+ ctx.fillText(`Grupo ${segment.groupId}`, canvas.width / 2, canvas.height / 2 - 60);
2482
+
2483
+ ctx.font = '18px Arial';
2484
+ ctx.fillText(`Tema: ${segment.theme}`, canvas.width / 2, canvas.height / 2 - 20);
2485
+
2486
+ ctx.font = '14px Arial';
2487
+ ctx.fillText(`Ângulo: ${segment.take.angle}`, canvas.width / 2, canvas.height / 2 + 20);
2488
+ ctx.fillText(`Score: ${segment.flowScore.toFixed(1)}%`, canvas.width / 2, canvas.height / 2 + 50);
2489
+ ctx.fillText(`Modo: Fallback`, canvas.width / 2, canvas.height / 2 + 80);
2490
+
2491
+ const progress = segment.localTime / segment.duration;
2492
+ ctx.fillText(`Progresso: ${(progress * 100).toFixed(1)}%`, canvas.width / 2, canvas.height / 2 + 110);
2493
+
2494
+ // Barra de progresso
2495
+ const barWidth = 300;
2496
+ const barHeight = 10;
2497
+ const barX = (canvas.width - barWidth) / 2;
2498
+ const barY = canvas.height / 2 + 130;
2499
+
2500
+ ctx.fillStyle = '#555555';
2501
+ ctx.fillRect(barX, barY, barWidth, barHeight);
2502
+
2503
+ ctx.fillStyle = '#00ff00';
2504
+ ctx.fillRect(barX, barY, barWidth * progress, barHeight);
2505
+
2506
+ ctx.textAlign = 'left';
2507
  }
2508
 
2509
+ // ✅ OVERLAY DE DEBUG - Informações em tempo real
2510
+ renderDebugOverlay(ctx, segment, currentTime) {
2511
+ const alpha = 0.7;
2512
+
2513
+ // Fundo do overlay
2514
+ ctx.fillStyle = `rgba(0, 0, 0, ${0.6 * alpha})`;
2515
+ ctx.fillRect(10, 10, 400, 120);
2516
+
2517
+ ctx.fillStyle = 'white';
2518
+ ctx.font = 'bold 12px Arial';
2519
+ ctx.fillText(`Grupo ${segment.groupId} - ${segment.theme}`, 20, 30);
2520
+
2521
+ ctx.font = '11px Arial';
2522
+ ctx.fillText(`Tempo: ${currentTime.toFixed(2)}s / ${segment.localTime.toFixed(2)}s`, 20, 50);
2523
+ ctx.fillText(`Score: ${segment.flowScore.toFixed(1)}% | Frame: ${this.renderingState.frameCount}`, 20, 70);
2524
+ ctx.fillText(`FPS: ${(1000 / this.renderingState.frameInterval).toFixed(0)} | Dropped: ${this.renderingState.droppedFrames}`, 20, 90);
2525
+ ctx.fillText(`Video Time: ${segment.videoTime.toFixed(2)}s | ${this.canvasDimensions.width}x${this.canvasDimensions.height}`, 20, 110);
2526
+ }
2527
+
2528
+ // ✅ STREAM DE ÁUDIO - Sincronização precisa
2529
+ async createAudioStream() {
2530
+ if (!this.syncManager.audioBuffer) {
2531
+ throw new Error('Buffer de áudio não disponível');
2532
  }
2533
 
2534
+ const destination = this.syncManager.audioContext.createMediaStreamDestination();
2535
+ const source = this.syncManager.audioContext.createBufferSource();
2536
+ source.buffer = this.syncManager.audioBuffer;
 
2537
 
2538
+ // Aplicar ganho para evitar clipping
2539
+ const gainNode = this.syncManager.audioContext.createGain();
2540
+ gainNode.gain.value = 0.7;
2541
+
2542
+ source.connect(gainNode);
2543
+ gainNode.connect(destination);
2544
+
2545
+ this.syncManager.audioSource = source;
2546
+
2547
+ return destination.stream;
2548
  }
2549
 
2550
+ // ✅ INICIAR ÁUDIO - Sincronizado com vídeo
2551
+ startAudioPlayback() {
2552
+ if (!this.syncManager.audioSource || !this.syncManager.audioBuffer) {
2553
+ return;
2554
+ }
2555
+
2556
+ const audioDelay = 50; // 50ms de delay para sincronizar com vídeo
2557
+
2558
+ setTimeout(() => {
2559
+ this.syncManager.audioSource.start(0);
2560
+ this.syncManager.isAudioPlaying = true;
2561
+ this.log('Áudio iniciado');
2562
+ }, audioDelay);
2563
  }
2564
 
2565
+ // ✅ LIMPEZA - Liberar recursos
2566
+ cleanupRendering() {
2567
+ this.renderingState.isRendering = false;
2568
+
2569
+ // Parar áudio
2570
+ if (this.syncManager.audioSource) {
2571
  try {
2572
+ this.syncManager.audioSource.stop();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2573
  } catch (error) {
2574
+ // Ignorar erros ao parar áudio
 
2575
  }
2576
+ this.syncManager.audioSource = null;
2577
+ }
 
 
 
 
2578
 
2579
+ if (this.syncManager.audioContext) {
 
 
 
 
 
 
 
 
2580
  try {
2581
+ this.syncManager.audioContext.close();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2582
  } catch (error) {
2583
+ // Ignorar erros ao fechar context
 
 
 
 
2584
  }
2585
+ this.syncManager.audioContext = null;
2586
+ }
 
 
 
 
 
 
 
 
2587
 
2588
+ // Limpar vídeos ativos
2589
+ this.activeVideos.forEach(video => {
2590
+ if (video.src) {
2591
+ URL.revokeObjectURL(video.src);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2592
  }
2593
+ });
2594
+ this.activeVideos.clear();
 
2595
 
2596
+ // Limpar cache (opcional - manter para performance)
2597
+ // this.videoCache.clear();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2598
 
2599
+ this.log('Renderização limpa');
 
2600
  }
2601
 
2602
+ // NOVO: Detecção do melhor formato de saída
2603
+ detectBestOutputFormat() {
2604
+ const formats = [
2605
+ {
2606
+ mimeType: 'video/webm;codecs=vp9,opus',
2607
+ extension: 'webm',
2608
+ videoBitrate: 6000000,
2609
+ priority: 1
2610
+ },
2611
+ {
2612
+ mimeType: 'video/mp4;codecs=avc1.42E01E,mp4a.40.2',
2613
+ extension: 'mp4',
2614
+ videoBitrate: 8000000,
2615
+ priority: 2
2616
+ },
2617
+ {
2618
+ mimeType: 'video/webm;codecs=vp8,opus',
2619
+ extension: 'webm',
2620
+ videoBitrate: 4000000,
2621
+ priority: 3
2622
+ },
2623
+ {
2624
+ mimeType: 'video/webm;codecs=av1,opus',
2625
+ extension: 'webm',
2626
+ videoBitrate: 5000000,
2627
+ priority: 4
2628
  }
2629
+ ];
2630
+
2631
+ // Testar suporte e escolher o melhor
2632
+ for (const format of formats) {
2633
+ if (MediaRecorder.isTypeSupported(format.mimeType)) {
2634
+ this.log(`Formato suportado: ${format.mimeType} (prioridade: ${format.priority})`);
2635
+ return format;
 
 
 
 
 
 
 
 
 
2636
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2637
  }
 
 
 
 
 
2638
 
2639
+ // Fallback para o formato mais básico
2640
+ const fallback = {
2641
+ mimeType: 'video/webm',
2642
+ extension: 'webm',
2643
+ videoBitrate: 3000000,
2644
+ priority: 999
2645
+ };
2646
+
2647
+ this.log(`Usando fallback: ${fallback.mimeType}`);
2648
+ return fallback;
 
 
 
 
 
 
 
 
2649
  }
2650
 
2651
  async exportAudioOnly() {
 
2672
  }, 2000);
2673
  }
2674
 
 
 
 
 
2675
  exportProjectFile(isFallback = false) {
2676
  const projectData = {
2677
  metadata: {
2678
  projectName: `Projeto_Inteligente_${new Date().toISOString().slice(0, 10)}`,
2679
  audience: this.targetAudience,
2680
  createdAt: new Date().toISOString(),
2681
+ version: '9.0 - Sistema Completo de Fallback e Matching Inteligente',
2682
  totalBlocks: this.scriptBlocks ? this.scriptBlocks.length : 0,
2683
  totalGroups: this.blockGroups ? this.blockGroups.length : 0,
2684
  totalDuration: this.finalTimeline ?
 
2710
  duration: group.take.duration,
2711
  orientation: group.take.orientation,
2712
  codec: group.take.codec,
2713
+ format: group.take.format,
2714
+ wasFallback: group.take.wasFallback || false,
2715
+ wasGeneric: group.take.wasGeneric || false
2716
  } : null,
2717
  flowScore: group.flowScore || 0,
2718
+ bestTakes: group.bestTakes || [],
2719
+ matchingInfo: group.matchingInfo || {}
2720
  })),
2721
  timeline: this.finalTimeline || [],
2722
  qualityMetrics: this.qualityMetrics || {},
 
2739
  formatValidation: true,
2740
  codecDetection: true,
2741
  debuggingSystem: true,
2742
+ renderingStats: this.renderingStats,
2743
+ themeCoverage: {
2744
+ totalThemes: [...new Set(this.scriptBlocks?.map(b => b.theme) || [])],
2745
+ themesWithTakes: [...new Set(this.blockGroups?.map(g => g.theme).filter(t => t) || [])],
2746
+ matchingSuccess: this.validateThemeCoverage()
2747
+ }
2748
  };
2749
 
2750
  const blob = new Blob([JSON.stringify(projectData, null, 2)], { type: 'application/json' });
 
2780
  const modal = document.getElementById('exportProgress');
2781
  if (modal) modal.classList.remove('active');
2782
  }
2783
+
2784
+ delay(ms) {
2785
+ return new Promise(resolve => setTimeout(resolve, ms));
2786
+ }
2787
  }
2788
 
2789
  // Funções globais
 
2806
  editor.exportProjectFile();
2807
  }
2808
 
2809
+ // Inicialização do editor
2810
  const editor = new VideoEditorInteligente();