player2 / test_blob.html
izuemon's picture
Create test_blob.html
23868e7
Raw History Blame Contribute Delete
16.4 kB
<!DOCTYPE html>
<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>