Download test_blob.html from izuemon/player2: direct link, hf CLI and curl.
- Browser
- Download file 16.4 kB
-
https://huggingface.co/spaces/izuemon/player2/resolve/main/test_blob.html
- Command line
-
hf download hf://spaces/izuemon/player2/test_blob.html
-
curl -L -o test_blob.html https://huggingface.co/spaces/izuemon/player2/resolve/main/test_blob.html
16.4 kB
| <html lang="ja"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>ZIP→Blob→動画表示テスト</title> | |
| <script src="/zip.min.js"></script> | |
| <style> | |
| body { background: #0a192f; color: #e6f1ff; font-family: sans-serif; padding: 20px; } | |
| h1 { color: #64ffda; font-size: 18px; } | |
| button { background: #112240; color: #e6f1ff; border: 1px solid #64ffda; | |
| padding: 8px 16px; border-radius: 4px; cursor: pointer; margin: 4px; } | |
| button:hover { background: rgba(100,255,218,0.2); } | |
| button:disabled { opacity: 0.5; cursor: not-allowed; } | |
| .log { background: #000; color: #64ffda; font-family: monospace; font-size: 12px; | |
| padding: 10px; border-radius: 4px; height: 300px; overflow-y: auto; | |
| white-space: pre-wrap; margin-top: 10px; border: 1px solid #1e2a47; } | |
| .video-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin: 10px 0; } | |
| .video-cell { background: #000; border: 1px solid #1e2a47; border-radius: 4px; overflow: hidden; } | |
| .video-cell video { width: 100%; display: block; background: #000; } | |
| .video-cell .label { padding: 4px 8px; background: #112240; font-size: 12px; } | |
| .info { background: #112240; padding: 10px; border-radius: 4px; margin: 10px 0; | |
| font-size: 12px; white-space: pre-wrap; } | |
| .status-bar { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; } | |
| .diag { background: #1e2a47; padding: 2px 8px; border-radius: 3px; font-size: 12px; } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>ZIP → Blob → 動画表示テスト</h1> | |
| <div class="status-bar"> | |
| <button id="btn-load-zip">1. ZIPを読み込む</button> | |
| <button id="btn-set-src" disabled>2. Blobから動画を設定</button> | |
| <button id="btn-reset" disabled>リセット</button> | |
| <span class="diag" id="diag-zip">ZIP: 未読込</span> | |
| <span class="diag" id="diag-cache">cache: 0</span> | |
| <span class="diag" id="diag-pending">pending: 0</span> | |
| </div> | |
| <div class="info" id="info-box">ここに診断情報が表示されます。</div> | |
| <div class="video-grid"> | |
| <div class="video-cell"> | |
| <div class="label">sv.mp4 (ソプラノ)</div> | |
| <video id="video-sv" muted playsinline preload="auto" controls></video> | |
| </div> | |
| <div class="video-cell"> | |
| <div class="label">av.mp4 (アルト)</div> | |
| <video id="video-av" muted playsinline preload="auto" controls></video> | |
| </div> | |
| <div class="video-cell"> | |
| <div class="label">tv.mp4 (テノール)</div> | |
| <video id="video-tv" muted playsinline preload="auto" controls></video> | |
| </div> | |
| </div> | |
| <div class="log" id="log"></div> | |
| <script> | |
| // ============================================================ | |
| // グローバル状態(元コードから抽出) | |
| // ============================================================ | |
| let zipAssets = null; | |
| let zipLoaded = false; | |
| let zipLoadPromise = null; | |
| const ZIP_PASSWORD_STORAGE_KEY = 'zip_password_test'; | |
| let cachedPassword = null; | |
| const BASE_PATH = './f/'; | |
| const videoFiles = { s: 'sv.mp4', a: 'av.mp4', t: 'tv.mp4' }; | |
| const videoBlobCache = {}; // fileName -> blob URL | |
| const pendingFetches = {}; // fileName -> Promise | |
| const logEl = document.getElementById('log'); | |
| function log(msg) { | |
| const t = new Date().toLocaleTimeString(); | |
| logEl.textContent += `[${t}] ${msg}\n`; | |
| logEl.scrollTop = logEl.scrollHeight; | |
| console.log(msg); | |
| } | |
| function updateDiag() { | |
| document.getElementById('diag-zip').textContent = | |
| `ZIP: ${zipLoaded ? '✅ ' + Object.keys(zipAssets || {}).length + 'ファイル' : '未読込'}`; | |
| document.getElementById('diag-cache').textContent = | |
| `cache: ${Object.keys(videoBlobCache).length}`; | |
| document.getElementById('diag-pending').textContent = | |
| `pending: ${Object.keys(pendingFetches).length}`; | |
| } | |
| function setInfo(text) { | |
| document.getElementById('info-box').textContent = text; | |
| } | |
| // ============================================================ | |
| // getContentType (元コードのまま) | |
| // ============================================================ | |
| function getContentType(fileName) { | |
| const ext = fileName.split('.').pop().toLowerCase(); | |
| const types = { | |
| 'html': 'text/html', 'css': 'text/css', 'js': 'application/javascript', | |
| 'json': 'application/json', 'png': 'image/png', 'jpg': 'image/jpeg', | |
| 'jpeg': 'image/jpeg', 'gif': 'image/gif', 'svg': 'image/svg+xml', | |
| 'ico': 'image/x-icon', 'webp': 'image/webp', 'ttf': 'font/ttf', | |
| 'woff': 'font/woff', 'woff2': 'font/woff2', 'mid': 'audio/midi', | |
| 'midi': 'audio/midi', 'opus': 'audio/opus', 'mp4': 'video/mp4', | |
| 'webm': 'video/webm', 'mei': 'application/xml', 'xml': 'application/xml', | |
| 'txt': 'text/plain' | |
| }; | |
| return types[ext] || 'application/octet-stream'; | |
| } | |
| // ============================================================ | |
| // パスワード関連(元コードのまま) | |
| // ============================================================ | |
| function getZipPassword() { | |
| if (!cachedPassword) { | |
| const stored = localStorage.getItem(ZIP_PASSWORD_STORAGE_KEY); | |
| if (stored) { | |
| try { | |
| cachedPassword = atob(stored); | |
| log('🔑 保存されたパスワードを使用します'); | |
| return cachedPassword; | |
| } catch (e) { | |
| localStorage.removeItem(ZIP_PASSWORD_STORAGE_KEY); | |
| } | |
| } | |
| } | |
| if (cachedPassword) return cachedPassword; | |
| while (true) { | |
| const password = prompt('ZIPファイルのパスワードを入力してください', ''); | |
| if (password === null) throw new Error('パスワード入力がキャンセルされました'); | |
| if (password.trim() === '') { alert('パスワードを入力してください'); continue; } | |
| cachedPassword = password; | |
| return password; | |
| } | |
| } | |
| function saveZipPassword(pw) { | |
| try { | |
| localStorage.setItem(ZIP_PASSWORD_STORAGE_KEY, btoa(pw)); | |
| cachedPassword = pw; | |
| log('🔑 パスワードを保存しました'); | |
| } catch (e) { log('パスワード保存失敗: ' + e); } | |
| } | |
| function clearZipPassword() { | |
| localStorage.removeItem(ZIP_PASSWORD_STORAGE_KEY); | |
| cachedPassword = null; | |
| log('🔑 パスワードをクリア'); | |
| } | |
| // ============================================================ | |
| // ZIP読み込み(元コードから抽出 + 診断ログ強化) | |
| // ============================================================ | |
| async function loadZipAssets() { | |
| if (zipLoadPromise) return zipLoadPromise; | |
| zipLoadPromise = (async () => { | |
| try { | |
| log('📦 Zip読み込み開始...'); | |
| const zipUrl = '/assets.zip'; | |
| let password = null; | |
| try { password = getZipPassword(); } | |
| catch (e) { throw new Error('パスワードの入力がキャンセルされました'); } | |
| const response = await fetch(zipUrl); | |
| if (!response.ok) throw new Error(`Zipファイルの読み込みに失敗: ${response.status}`); | |
| const blob = await response.blob(); | |
| log(`📦 Zipファイルサイズ: ${(blob.size / 1024 / 1024).toFixed(2)}MB`); | |
| if (blob.size === 0) throw new Error('Zipファイルが空です'); | |
| zip.configure({ useWebWorkers: false }); | |
| const zipReader = new zip.ZipReader(new zip.BlobReader(blob)); | |
| let entries; | |
| try { entries = await zipReader.getEntries(); } | |
| catch (err) { await zipReader.close(); throw err; } | |
| const fileEntries = entries.filter(e => !e.directory); | |
| log('📦 Zip解析完了、ファイル数: ' + fileEntries.length); | |
| if (fileEntries.length === 0) { | |
| await zipReader.close(); | |
| throw new Error('Zipファイルにエントリが見つかりません'); | |
| } | |
| // パスワード検証 | |
| try { | |
| await fileEntries[0].getData(new zip.Uint8ArrayWriter(), { | |
| password, checkPasswordOnly: true | |
| }); | |
| log('✅ ZIP復号化成功'); | |
| } catch (err) { | |
| await zipReader.close(); | |
| if (err.message && /password/i.test(err.message)) { | |
| clearZipPassword(); | |
| zipLoadPromise = null; | |
| return loadZipAssets(); // 再帰 | |
| } | |
| throw err; | |
| } | |
| saveZipPassword(password); | |
| const assets = {}; | |
| const filePromises = fileEntries.map(async (entry) => { | |
| const fileName = entry.filename.split('/').pop(); | |
| if (!fileName) return null; | |
| try { | |
| const ext = fileName.split('.').pop().toLowerCase(); | |
| if (['png','jpg','jpeg','gif','svg','ico','webp'].includes(ext)) { | |
| const blob = await entry.getData( | |
| new zip.BlobWriter(getContentType(fileName)), { password }); | |
| assets[fileName] = URL.createObjectURL(blob); | |
| } else if (['mp4','webm','ogg','opus','mid','midi'].includes(ext)) { | |
| const blob = await entry.getData( | |
| new zip.BlobWriter(getContentType(fileName)), { password }); | |
| assets[fileName] = blob; | |
| // ★★★ ここが重要: Blobの中身を診断 ★★★ | |
| const headBytes = new Uint8Array(await blob.slice(0, 32).arrayBuffer()); | |
| const hex = Array.from(headBytes) | |
| .map(b => b.toString(16).padStart(2, '0')).join(' '); | |
| const ascii = Array.from(headBytes) | |
| .map(b => (b >= 32 && b < 127) ? String.fromCharCode(b) : '.').join(''); | |
| log(` 🔍 ${fileName}`); | |
| log(` blob.size=${blob.size} blob.type=${blob.type}`); | |
| log(` entry.compressedSize=${entry.compressedSize} entry.uncompressedSize=${entry.uncompressedSize}`); | |
| log(` head(hex) : ${hex}`); | |
| log(` head(ascii): ${ascii}`); | |
| log(` ftyp存在 : ${ascii.includes('ftyp') ? '✅ YES' : '❌ NO'}`); | |
| // MP4の先頭4バイトは通常 00 00 00 xx (box size)、続いて 'ftyp' | |
| if (ascii.substring(4, 8) === 'ftyp') { | |
| log(` → 正常なMP4ヘッダの可能性が高い`); | |
| } else { | |
| log(` → ⚠️ MP4ヘッダではない!`); | |
| } | |
| // 末尾も診断 | |
| if (blob.size > 32) { | |
| const tailBytes = new Uint8Array(await blob.slice(blob.size - 32).arrayBuffer()); | |
| const tailAscii = Array.from(tailBytes) | |
| .map(b => (b >= 32 && b < 127) ? String.fromCharCode(b) : '.').join(''); | |
| log(` tail(ascii): ${tailAscii}`); | |
| } | |
| } else { | |
| const text = await entry.getData(new zip.TextWriter(), { password }); | |
| assets[fileName] = text; | |
| } | |
| } catch (err) { | |
| log(` ❌ ${fileName} の読み込みに失敗: ${err.message}`); | |
| } | |
| }); | |
| await Promise.all(filePromises); | |
| await zipReader.close(); | |
| zipAssets = assets; | |
| zipLoaded = true; | |
| log('✅ 完了: ' + Object.keys(assets).length + 'ファイル'); | |
| log(' 一覧: ' + Object.keys(assets).join(', ')); | |
| updateDiag(); | |
| return assets; | |
| } catch (err) { | |
| log('❌ Zip読み込みエラー: ' + err.message); | |
| zipLoaded = false; | |
| zipAssets = null; | |
| zipLoadPromise = null; | |
| throw err; | |
| } | |
| })(); | |
| return zipLoadPromise; | |
| } | |
| // ============================================================ | |
| // setVideoSource(元コードのまま + Promise返却修正) | |
| // ============================================================ | |
| function setVideoSource(video, fileName) { | |
| if (videoBlobCache[fileName]) { | |
| const url = videoBlobCache[fileName]; | |
| if (video.src !== url) { | |
| video.src = url; | |
| video.load(); | |
| log(`📹 キャッシュから設定: ${fileName}`); | |
| } | |
| return Promise.resolve(url); | |
| } | |
| if (pendingFetches[fileName]) { | |
| log(`⏳ フェッチ完了を待機: ${fileName}`); | |
| return pendingFetches[fileName].then(url => { | |
| if (video.src !== url) { video.src = url; log(`📹 フェッチ後設定: ${fileName}`); } | |
| return url; | |
| }); | |
| } | |
| log(`🔄 新規フェッチ開始: ${fileName}`); | |
| const fullPath = BASE_PATH + fileName; | |
| const fetchPromise = (async () => { | |
| try { | |
| let blob = null; | |
| if (zipLoaded && zipAssets && zipAssets[fileName]) { | |
| const asset = zipAssets[fileName]; | |
| if (asset instanceof Blob) { | |
| blob = asset; | |
| log(`✅ ZipからBlob取得: ${fileName} (size=${blob.size}, type=${blob.type})`); | |
| } else if (typeof asset === 'string' && asset.startsWith('blob:')) { | |
| const resp = await fetch(asset); | |
| blob = await resp.blob(); | |
| } | |
| } | |
| if (!blob) { | |
| log(`🔄 ${fileName} を通常fetchします`); | |
| const resp = await fetch(fullPath); | |
| if (!resp.ok) throw new Error(`HTTP ${resp.status}`); | |
| blob = await resp.blob(); | |
| log(` 通常fetch結果: size=${blob.size}, type=${blob.type}`); | |
| } | |
| if (!blob) { | |
| video.src = fullPath; | |
| video.load(); | |
| delete pendingFetches[fileName]; | |
| updateDiag(); | |
| return fullPath; | |
| } | |
| const url = URL.createObjectURL(blob); | |
| videoBlobCache[fileName] = url; | |
| delete pendingFetches[fileName]; | |
| updateDiag(); | |
| if (video.src !== url) { | |
| video.src = url; | |
| video.load(); | |
| } | |
| log(`✅ ビデオ設定完了: ${fileName} → ${url}`); | |
| return url; | |
| } catch (err) { | |
| delete pendingFetches[fileName]; | |
| updateDiag(); | |
| log(`❌ 動画読込失敗: ${fileName} / ${err.message}`); | |
| throw err; | |
| } | |
| })(); | |
| pendingFetches[fileName] = fetchPromise; | |
| updateDiag(); | |
| return fetchPromise; | |
| } | |
| // ============================================================ | |
| // ボタン操作 | |
| // ============================================================ | |
| document.getElementById('btn-load-zip').addEventListener('click', async () => { | |
| document.getElementById('btn-load-zip').disabled = true; | |
| try { | |
| await loadZipAssets(); | |
| document.getElementById('btn-set-src').disabled = false; | |
| document.getElementById('btn-reset').disabled = false; | |
| } catch (e) { | |
| log('読み込み失敗: ' + e.message); | |
| document.getElementById('btn-load-zip').disabled = false; | |
| } | |
| }); | |
| document.getElementById('btn-set-src').addEventListener('click', async () => { | |
| const vids = [ | |
| { id: 'video-sv', file: videoFiles.s }, | |
| { id: 'video-av', file: videoFiles.a }, | |
| { id: 'video-tv', file: videoFiles.t }, | |
| ]; | |
| for (const { id, file } of vids) { | |
| const video = document.getElementById(id); | |
| try { | |
| await setVideoSource(video, file); | |
| } catch (e) { | |
| log(`セット失敗: ${file} / ${e.message}`); | |
| } | |
| } | |
| updateDiag(); | |
| // 各動画の状態を一定時間後にチェック | |
| setTimeout(() => { | |
| vids.forEach(({ id, file }) => { | |
| const v = document.getElementById(id); | |
| log(` [${file}] readyState=${v.readyState} networkState=${v.networkState} ` + | |
| `duration=${isNaN(v.duration) ? 'NaN' : v.duration.toFixed(3)} ` + | |
| `videoWidth=${v.videoWidth} videoHeight=${v.videoHeight} ` + | |
| `error=${v.error ? v.error.code + ':' + v.error.message : 'none'}`); | |
| }); | |
| }, 3000); | |
| }); | |
| document.getElementById('btn-reset').addEventListener('click', () => { | |
| Object.values(videoBlobCache).forEach(url => URL.revokeObjectURL(url)); | |
| Object.keys(videoBlobCache).forEach(k => delete videoBlobCache[k]); | |
| Object.keys(pendingFetches).forEach(k => delete pendingFetches[k]); | |
| ['video-sv','video-av','video-tv'].forEach(id => { | |
| const v = document.getElementById(id); | |
| v.removeAttribute('src'); | |
| v.load(); | |
| }); | |
| zipLoaded = false; | |
| zipAssets = null; | |
| zipLoadPromise = null; | |
| clearZipPassword(); | |
| updateDiag(); | |
| log('🔄 リセット完了'); | |
| document.getElementById('btn-load-zip').disabled = false; | |
| document.getElementById('btn-set-src').disabled = true; | |
| document.getElementById('btn-reset').disabled = true; | |
| }); | |
| // 各video要素のイベントをログ | |
| ['video-sv','video-av','video-tv'].forEach(id => { | |
| const v = document.getElementById(id); | |
| ['loadedmetadata','loadeddata','canplay','canplaythrough','error','stalled','suspend','abort','emptied'] | |
| .forEach(ev => { | |
| v.addEventListener(ev, () => { | |
| const errInfo = v.error ? ` error=${v.error.code}:${v.error.message}` : ''; | |
| log(` 🎬 [${id}] ${ev} readyState=${v.readyState} networkState=${v.networkState}${errInfo}`); | |
| }); | |
| }); | |
| }); | |
| updateDiag(); | |
| log('テストページ準備完了。手順: 1. ZIPを読み込む → 2. Blobから動画を設定'); | |
| </script> | |
| </body> | |
| </html> |