[{"data":1,"prerenderedAt":1913},["ShallowReactive",2],{"navigation":3,"blog-en-how-i-built-flowpick-in-three-months":713,"blog-en-how-i-built-flowpick-in-three-months-surround":1901},[4,170,307,444,580],{"title":5,"path":6,"stem":7,"children":8,"page":32},"Docs","\u002Fdocs","1.docs",[9,33,74,95,134,154],{"title":10,"path":11,"stem":12,"children":13,"icon":32},"Getting Started","\u002Fdocs\u002Fgetting-started","1.docs\u002F1.getting-started\u002F1.index",[14,17,22,27],{"title":15,"path":11,"stem":12,"icon":16},"Introduction","i-lucide-house",{"title":18,"path":19,"stem":20,"icon":21},"Installation","\u002Fdocs\u002Fgetting-started\u002Finstallation","1.docs\u002F1.getting-started\u002F2.installation","i-lucide-download",{"title":23,"path":24,"stem":25,"icon":26},"Usage Guide","\u002Fdocs\u002Fgetting-started\u002Fusage","1.docs\u002F1.getting-started\u002F3.usage","i-lucide-sliders",{"title":28,"path":29,"stem":30,"icon":31},"Configuration Reference","\u002Fdocs\u002Fgetting-started\u002Fconfiguration","1.docs\u002F1.getting-started\u002F4.configuration","i-lucide-settings",false,{"title":34,"icon":35,"path":36,"stem":37,"children":38,"page":32},"Features","i-lucide-star","\u002Fdocs\u002Ffeatures","1.docs\u002F2.features",[39,44,49,54,59,64,69],{"title":40,"path":41,"stem":42,"icon":43},"Video Sniffing","\u002Fdocs\u002Ffeatures\u002Fvideo-sniffing","1.docs\u002F2.features\u002F1.video-sniffing","i-lucide-video",{"title":45,"path":46,"stem":47,"icon":48},"Audio Capture","\u002Fdocs\u002Ffeatures\u002Faudio-capture","1.docs\u002F2.features\u002F2.audio-capture","i-lucide-music",{"title":50,"path":51,"stem":52,"icon":53},"Image Download","\u002Fdocs\u002Ffeatures\u002Fimage-download","1.docs\u002F2.features\u002F3.image-download","i-lucide-image",{"title":55,"path":56,"stem":57,"icon":58},"Privacy & Security","\u002Fdocs\u002Ffeatures\u002Fprivacy-security","1.docs\u002F2.features\u002F4.privacy-security","i-lucide-shield-check",{"title":60,"path":61,"stem":62,"icon":63},"Format Conversion","\u002Fdocs\u002Ffeatures\u002Fformat-conversion","1.docs\u002F2.features\u002F5.format-conversion","i-lucide-arrow-left-right",{"title":65,"path":66,"stem":67,"icon":68},"Batch Download","\u002Fdocs\u002Ffeatures\u002Fbatch-download","1.docs\u002F2.features\u002F6.batch-download","i-lucide-layers",{"title":70,"path":71,"stem":72,"icon":73},"Keyboard Shortcuts","\u002Fdocs\u002Ffeatures\u002Fkeyboard-shortcuts","1.docs\u002F2.features\u002F7.keyboard-shortcuts","i-lucide-keyboard",{"title":75,"icon":76,"path":77,"stem":78,"children":79,"page":32},"Advanced","i-lucide-book-open","\u002Fdocs\u002Fadvanced","1.docs\u002F3.advanced",[80,85,90],{"title":81,"path":82,"stem":83,"icon":84},"Online Tools","\u002Fdocs\u002Fadvanced\u002Fonline-tools","1.docs\u002F3.advanced\u002F1.online-tools","i-lucide-globe",{"title":86,"path":87,"stem":88,"icon":89},"Browser Compatibility","\u002Fdocs\u002Fadvanced\u002Fbrowser-compatibility","1.docs\u002F3.advanced\u002F2.browser-compatibility","i-lucide-monitor-check",{"title":91,"path":92,"stem":93,"icon":94},"Download Engine Architecture","\u002Fdocs\u002Fadvanced\u002Fdownload-engine","1.docs\u002F3.advanced\u002F3.download-engine","i-lucide-cpu",{"title":96,"icon":97,"path":98,"stem":99,"children":100,"page":32},"Use Cases","i-lucide-lightbulb","\u002Fdocs\u002Fusecases","1.docs\u002F4.usecases",[101,106,110,115,120,124,129],{"title":102,"path":103,"stem":104,"icon":105},"Online Courses & Educational Platforms","\u002Fdocs\u002Fusecases\u002Fonline-courses","1.docs\u002F4.usecases\u002F1.online-courses","i-lucide-graduation-cap",{"title":107,"path":108,"stem":109,"icon":43},"Video Platforms & UGC Content","\u002Fdocs\u002Fusecases\u002Fvideo-platforms","1.docs\u002F4.usecases\u002F2.video-platforms",{"title":111,"path":112,"stem":113,"icon":114},"Live Replays & Event Recordings","\u002Fdocs\u002Fusecases\u002Flive-replays","1.docs\u002F4.usecases\u002F3.live-replays","i-lucide-radio",{"title":116,"path":117,"stem":118,"icon":119},"Podcasts & Audio Content","\u002Fdocs\u002Fusecases\u002Fpodcasts-audio","1.docs\u002F4.usecases\u002F4.podcasts-audio","i-lucide-headphones",{"title":121,"path":122,"stem":123,"icon":53},"Image Galleries & Wallpaper Batch Download","\u002Fdocs\u002Fusecases\u002Fimage-galleries","1.docs\u002F4.usecases\u002F5.image-galleries",{"title":125,"path":126,"stem":127,"icon":128},"Corporate Training & Internal Videos","\u002Fdocs\u002Fusecases\u002Fcorporate-training","1.docs\u002F4.usecases\u002F6.corporate-training","i-lucide-building-2",{"title":130,"path":131,"stem":132,"icon":133},"Personal Media Archiving & Collection","\u002Fdocs\u002Fusecases\u002Fpersonal-archiving","1.docs\u002F4.usecases\u002F7.personal-archiving","i-lucide-archive",{"title":135,"icon":136,"path":137,"stem":138,"children":139,"page":32},"Troubleshooting","i-lucide-wrench","\u002Fdocs\u002Ftroubleshooting","1.docs\u002F5.troubleshooting",[140,144,149],{"title":141,"path":142,"stem":143,"icon":136},"Common Issues Troubleshooting","\u002Fdocs\u002Ftroubleshooting\u002Fcommon-issues","1.docs\u002F5.troubleshooting\u002F1.common-issues",{"title":145,"path":146,"stem":147,"icon":148},"Known Limitations & Caveats","\u002Fdocs\u002Ftroubleshooting\u002Fknown-issues","1.docs\u002F5.troubleshooting\u002F2.known-issues","i-lucide-alert-triangle",{"title":150,"path":151,"stem":152,"icon":153},"FAQ","\u002Fdocs\u002Ftroubleshooting\u002Ffaq","1.docs\u002F5.troubleshooting\u002F3.faq","i-lucide-message-circle-question",{"title":155,"icon":156,"path":157,"stem":158,"children":159,"page":32},"Developer Docs","i-lucide-code","\u002Fdocs\u002Fdeveloper","1.docs\u002F6.developer",[160,165],{"title":161,"path":162,"stem":163,"icon":164},"Contributing Guide","\u002Fdocs\u002Fdeveloper\u002Fcontributing","1.docs\u002F6.developer\u002F1.contributing","i-lucide-git-pull-request",{"title":166,"path":167,"stem":168,"icon":169},"Project Architecture","\u002Fdocs\u002Fdeveloper\u002Farchitecture","1.docs\u002F6.developer\u002F2.architecture","i-lucide-box",{"title":171,"path":172,"stem":173,"children":174,"page":32},"Ja","\u002Fja","ja",[175,194,227,244,277,294],{"title":176,"path":177,"stem":178,"children":179,"icon":32},"クイックスタート","\u002Fja\u002Fgetting-started","ja\u002F1.getting-started\u002F1.index",[180,182,186,190],{"title":181,"path":177,"stem":178,"icon":16},"概要",{"title":183,"path":184,"stem":185,"icon":21},"インストール","\u002Fja\u002Fgetting-started\u002Finstallation","ja\u002F1.getting-started\u002F2.installation",{"title":187,"path":188,"stem":189,"icon":26},"使用方法","\u002Fja\u002Fgetting-started\u002Fusage","ja\u002F1.getting-started\u002F3.usage",{"title":191,"path":192,"stem":193,"icon":31},"設定リファレンス","\u002Fja\u002Fgetting-started\u002Fconfiguration","ja\u002F1.getting-started\u002F4.configuration",{"title":195,"icon":35,"path":196,"stem":197,"children":198,"page":32},"機能","\u002Fja\u002Ffeatures","ja\u002F2.features",[199,203,207,211,215,219,223],{"title":200,"path":201,"stem":202,"icon":43},"動画検知","\u002Fja\u002Ffeatures\u002Fvideo-sniffing","ja\u002F2.features\u002F1.video-sniffing",{"title":204,"path":205,"stem":206,"icon":48},"音声キャプチャ","\u002Fja\u002Ffeatures\u002Faudio-capture","ja\u002F2.features\u002F2.audio-capture",{"title":208,"path":209,"stem":210,"icon":53},"画像ダウンロード","\u002Fja\u002Ffeatures\u002Fimage-download","ja\u002F2.features\u002F3.image-download",{"title":212,"path":213,"stem":214,"icon":58},"プライバシーとセキュリティ","\u002Fja\u002Ffeatures\u002Fprivacy-security","ja\u002F2.features\u002F4.privacy-security",{"title":216,"path":217,"stem":218,"icon":63},"フォーマット変換","\u002Fja\u002Ffeatures\u002Fformat-conversion","ja\u002F2.features\u002F5.format-conversion",{"title":220,"path":221,"stem":222,"icon":68},"一括ダウンロード","\u002Fja\u002Ffeatures\u002Fbatch-download","ja\u002F2.features\u002F6.batch-download",{"title":224,"path":225,"stem":226,"icon":73},"キーボードショートカット","\u002Fja\u002Ffeatures\u002Fkeyboard-shortcuts","ja\u002F2.features\u002F7.keyboard-shortcuts",{"title":228,"icon":76,"path":229,"stem":230,"children":231,"page":32},"詳細情報","\u002Fja\u002Fadvanced","ja\u002F3.advanced",[232,236,240],{"title":233,"path":234,"stem":235,"icon":84},"オンラインツール","\u002Fja\u002Fadvanced\u002Fonline-tools","ja\u002F3.advanced\u002F1.online-tools",{"title":237,"path":238,"stem":239,"icon":89},"ブラウザ互換性","\u002Fja\u002Fadvanced\u002Fbrowser-compatibility","ja\u002F3.advanced\u002F2.browser-compatibility",{"title":241,"path":242,"stem":243,"icon":94},"ダウンロードエンジンアーキテクチャ","\u002Fja\u002Fadvanced\u002Fdownload-engine","ja\u002F3.advanced\u002F3.download-engine",{"title":245,"icon":97,"path":246,"stem":247,"children":248,"page":32},"ユースケース","\u002Fja\u002Fusecases","ja\u002F4.usecases",[249,253,257,261,265,269,273],{"title":250,"path":251,"stem":252,"icon":105},"オンラインコースと教育プラットフォーム","\u002Fja\u002Fusecases\u002Fonline-courses","ja\u002F4.usecases\u002F1.online-courses",{"title":254,"path":255,"stem":256,"icon":43},"動画プラットフォームと UGC コンテンツ","\u002Fja\u002Fusecases\u002Fvideo-platforms","ja\u002F4.usecases\u002F2.video-platforms",{"title":258,"path":259,"stem":260,"icon":114},"ライブ配信アーカイブと競技録画","\u002Fja\u002Fusecases\u002Flive-replays","ja\u002F4.usecases\u002F3.live-replays",{"title":262,"path":263,"stem":264,"icon":119},"ポッドキャストとオーディオコンテンツ","\u002Fja\u002Fusecases\u002Fpodcasts-audio","ja\u002F4.usecases\u002F4.podcasts-audio",{"title":266,"path":267,"stem":268,"icon":53},"ギャラリーと壁紙の一括ダウンロード","\u002Fja\u002Fusecases\u002Fimage-galleries","ja\u002F4.usecases\u002F5.image-galleries",{"title":270,"path":271,"stem":272,"icon":128},"企業研修と内部動画","\u002Fja\u002Fusecases\u002Fcorporate-training","ja\u002F4.usecases\u002F6.corporate-training",{"title":274,"path":275,"stem":276,"icon":133},"個人メディアコレクションとアーカイブ","\u002Fja\u002Fusecases\u002Fpersonal-archiving","ja\u002F4.usecases\u002F7.personal-archiving",{"title":278,"icon":136,"path":279,"stem":280,"children":281,"page":32},"トラブルシューティング","\u002Fja\u002Ftroubleshooting","ja\u002F5.troubleshooting",[282,286,290],{"title":283,"path":284,"stem":285,"icon":136},"一般的な問題のトラブルシューティング","\u002Fja\u002Ftroubleshooting\u002Fcommon-issues","ja\u002F5.troubleshooting\u002F1.common-issues",{"title":287,"path":288,"stem":289,"icon":148},"既知の制限事項と注意点","\u002Fja\u002Ftroubleshooting\u002Fknown-issues","ja\u002F5.troubleshooting\u002F2.known-issues",{"title":291,"path":292,"stem":293,"icon":153},"よくある質問","\u002Fja\u002Ftroubleshooting\u002Ffaq","ja\u002F5.troubleshooting\u002F3.faq",{"title":295,"icon":156,"path":296,"stem":297,"children":298,"page":32},"開発者ガイド","\u002Fja\u002Fdeveloper","ja\u002F6.developer",[299,303],{"title":300,"path":301,"stem":302,"icon":164},"貢献ガイド","\u002Fja\u002Fdeveloper\u002Fcontributing","ja\u002F6.developer\u002F1.contributing",{"title":304,"path":305,"stem":306,"icon":169},"プロジェクトアーキテクチャ","\u002Fja\u002Fdeveloper\u002Farchitecture","ja\u002F6.developer\u002F2.architecture",{"title":308,"path":309,"stem":310,"children":311,"page":32},"Ko","\u002Fko","ko",[312,331,364,381,414,431],{"title":313,"path":314,"stem":315,"children":316,"icon":32},"빠른 시작","\u002Fko\u002Fgetting-started","ko\u002F1.getting-started\u002F1.index",[317,319,323,327],{"title":318,"path":314,"stem":315,"icon":16},"소개",{"title":320,"path":321,"stem":322,"icon":21},"설치","\u002Fko\u002Fgetting-started\u002Finstallation","ko\u002F1.getting-started\u002F2.installation",{"title":324,"path":325,"stem":326,"icon":26},"사용 방법","\u002Fko\u002Fgetting-started\u002Fusage","ko\u002F1.getting-started\u002F3.usage",{"title":328,"path":329,"stem":330,"icon":31},"설정 참조","\u002Fko\u002Fgetting-started\u002Fconfiguration","ko\u002F1.getting-started\u002F4.configuration",{"title":332,"icon":35,"path":333,"stem":334,"children":335,"page":32},"기능","\u002Fko\u002Ffeatures","ko\u002F2.features",[336,340,344,348,352,356,360],{"title":337,"path":338,"stem":339,"icon":43},"비디오 스니핑","\u002Fko\u002Ffeatures\u002Fvideo-sniffing","ko\u002F2.features\u002F1.video-sniffing",{"title":341,"path":342,"stem":343,"icon":48},"오디오 캡처","\u002Fko\u002Ffeatures\u002Faudio-capture","ko\u002F2.features\u002F2.audio-capture",{"title":345,"path":346,"stem":347,"icon":53},"이미지 다운로드","\u002Fko\u002Ffeatures\u002Fimage-download","ko\u002F2.features\u002F3.image-download",{"title":349,"path":350,"stem":351,"icon":58},"개인정보 보호와 보안","\u002Fko\u002Ffeatures\u002Fprivacy-security","ko\u002F2.features\u002F4.privacy-security",{"title":353,"path":354,"stem":355,"icon":63},"포맷 변환","\u002Fko\u002Ffeatures\u002Fformat-conversion","ko\u002F2.features\u002F5.format-conversion",{"title":357,"path":358,"stem":359,"icon":68},"일괄 다운로드","\u002Fko\u002Ffeatures\u002Fbatch-download","ko\u002F2.features\u002F6.batch-download",{"title":361,"path":362,"stem":363,"icon":73},"키보드 단축키","\u002Fko\u002Ffeatures\u002Fkeyboard-shortcuts","ko\u002F2.features\u002F7.keyboard-shortcuts",{"title":365,"icon":76,"path":366,"stem":367,"children":368,"page":32},"고급 가이드","\u002Fko\u002Fadvanced","ko\u002F3.advanced",[369,373,377],{"title":370,"path":371,"stem":372,"icon":84},"온라인 도구","\u002Fko\u002Fadvanced\u002Fonline-tools","ko\u002F3.advanced\u002F1.online-tools",{"title":374,"path":375,"stem":376,"icon":89},"브라우저 호환성","\u002Fko\u002Fadvanced\u002Fbrowser-compatibility","ko\u002F3.advanced\u002F2.browser-compatibility",{"title":378,"path":379,"stem":380,"icon":94},"다운로드 엔진 아키텍처","\u002Fko\u002Fadvanced\u002Fdownload-engine","ko\u002F3.advanced\u002F3.download-engine",{"title":382,"icon":97,"path":383,"stem":384,"children":385,"page":32},"사용 사례","\u002Fko\u002Fusecases","ko\u002F4.usecases",[386,390,394,398,402,406,410],{"title":387,"path":388,"stem":389,"icon":105},"온라인 강좌 및 교육 플랫폼","\u002Fko\u002Fusecases\u002Fonline-courses","ko\u002F4.usecases\u002F1.online-courses",{"title":391,"path":392,"stem":393,"icon":43},"비디오 플랫폼 및 UGC 콘텐츠","\u002Fko\u002Fusecases\u002Fvideo-platforms","ko\u002F4.usecases\u002F2.video-platforms",{"title":395,"path":396,"stem":397,"icon":114},"라이브 방송 재생 및 경기 녹화","\u002Fko\u002Fusecases\u002Flive-replays","ko\u002F4.usecases\u002F3.live-replays",{"title":399,"path":400,"stem":401,"icon":119},"팟캐스트 및 오디오 콘텐츠","\u002Fko\u002Fusecases\u002Fpodcasts-audio","ko\u002F4.usecases\u002F4.podcasts-audio",{"title":403,"path":404,"stem":405,"icon":53},"이미지 갤러리 및 배경화면 일괄 다운로드","\u002Fko\u002Fusecases\u002Fimage-galleries","ko\u002F4.usecases\u002F5.image-galleries",{"title":407,"path":408,"stem":409,"icon":128},"기업 교육 및 내부 비디오","\u002Fko\u002Fusecases\u002Fcorporate-training","ko\u002F4.usecases\u002F6.corporate-training",{"title":411,"path":412,"stem":413,"icon":133},"개인 미디어 수집 및 보관","\u002Fko\u002Fusecases\u002Fpersonal-archiving","ko\u002F4.usecases\u002F7.personal-archiving",{"title":415,"icon":136,"path":416,"stem":417,"children":418,"page":32},"문제 해결","\u002Fko\u002Ftroubleshooting","ko\u002F5.troubleshooting",[419,423,427],{"title":420,"path":421,"stem":422,"icon":136},"일반적인 문제 해결","\u002Fko\u002Ftroubleshooting\u002Fcommon-issues","ko\u002F5.troubleshooting\u002F1.common-issues",{"title":424,"path":425,"stem":426,"icon":148},"알려진 제한 사항 및 주의사항","\u002Fko\u002Ftroubleshooting\u002Fknown-issues","ko\u002F5.troubleshooting\u002F2.known-issues",{"title":428,"path":429,"stem":430,"icon":153},"자주 묻는 질문","\u002Fko\u002Ftroubleshooting\u002Ffaq","ko\u002F5.troubleshooting\u002F3.faq",{"title":432,"icon":156,"path":433,"stem":434,"children":435,"page":32},"개발자 문서","\u002Fko\u002Fdeveloper","ko\u002F6.developer",[436,440],{"title":437,"path":438,"stem":439,"icon":164},"기여 가이드라인","\u002Fko\u002Fdeveloper\u002Fcontributing","ko\u002F6.developer\u002F1.contributing",{"title":441,"path":442,"stem":443,"icon":169},"프로젝트 아키텍처","\u002Fko\u002Fdeveloper\u002Farchitecture","ko\u002F6.developer\u002F2.architecture",{"title":445,"path":446,"stem":447,"children":448,"page":32},"Zh Hans","\u002Fzh-hans","zh-Hans",[449,467,500,517,550,567],{"title":450,"path":451,"stem":452,"children":453,"icon":32},"快速开始","\u002Fzh-hans\u002Fgetting-started","zh-Hans\u002F1.getting-started\u002F1.index",[454,456,460,463],{"title":455,"path":451,"stem":452,"icon":16},"简介",{"title":457,"path":458,"stem":459,"icon":21},"安装","\u002Fzh-hans\u002Fgetting-started\u002Finstallation","zh-Hans\u002F1.getting-started\u002F2.installation",{"title":187,"path":461,"stem":462,"icon":26},"\u002Fzh-hans\u002Fgetting-started\u002Fusage","zh-Hans\u002F1.getting-started\u002F3.usage",{"title":464,"path":465,"stem":466,"icon":31},"配置参考","\u002Fzh-hans\u002Fgetting-started\u002Fconfiguration","zh-Hans\u002F1.getting-started\u002F4.configuration",{"title":468,"icon":35,"path":469,"stem":470,"children":471,"page":32},"功能特性","\u002Fzh-hans\u002Ffeatures","zh-Hans\u002F2.features",[472,476,480,484,488,492,496],{"title":473,"path":474,"stem":475,"icon":43},"视频嗅探","\u002Fzh-hans\u002Ffeatures\u002Fvideo-sniffing","zh-Hans\u002F2.features\u002F1.video-sniffing",{"title":477,"path":478,"stem":479,"icon":48},"音频捕获","\u002Fzh-hans\u002Ffeatures\u002Faudio-capture","zh-Hans\u002F2.features\u002F2.audio-capture",{"title":481,"path":482,"stem":483,"icon":53},"图片下载","\u002Fzh-hans\u002Ffeatures\u002Fimage-download","zh-Hans\u002F2.features\u002F3.image-download",{"title":485,"path":486,"stem":487,"icon":58},"隐私与安全","\u002Fzh-hans\u002Ffeatures\u002Fprivacy-security","zh-Hans\u002F2.features\u002F4.privacy-security",{"title":489,"path":490,"stem":491,"icon":63},"格式转换","\u002Fzh-hans\u002Ffeatures\u002Fformat-conversion","zh-Hans\u002F2.features\u002F5.format-conversion",{"title":493,"path":494,"stem":495,"icon":68},"批量下载","\u002Fzh-hans\u002Ffeatures\u002Fbatch-download","zh-Hans\u002F2.features\u002F6.batch-download",{"title":497,"path":498,"stem":499,"icon":73},"键盘快捷键","\u002Fzh-hans\u002Ffeatures\u002Fkeyboard-shortcuts","zh-Hans\u002F2.features\u002F7.keyboard-shortcuts",{"title":501,"icon":76,"path":502,"stem":503,"children":504,"page":32},"高级指南","\u002Fzh-hans\u002Fadvanced","zh-Hans\u002F3.advanced",[505,509,513],{"title":506,"path":507,"stem":508,"icon":84},"在线工具","\u002Fzh-hans\u002Fadvanced\u002Fonline-tools","zh-Hans\u002F3.advanced\u002F1.online-tools",{"title":510,"path":511,"stem":512,"icon":89},"浏览器兼容性","\u002Fzh-hans\u002Fadvanced\u002Fbrowser-compatibility","zh-Hans\u002F3.advanced\u002F2.browser-compatibility",{"title":514,"path":515,"stem":516,"icon":94},"下载引擎架构","\u002Fzh-hans\u002Fadvanced\u002Fdownload-engine","zh-Hans\u002F3.advanced\u002F3.download-engine",{"title":518,"icon":97,"path":519,"stem":520,"children":521,"page":32},"使用场景","\u002Fzh-hans\u002Fusecases","zh-Hans\u002F4.usecases",[522,526,530,534,538,542,546],{"title":523,"path":524,"stem":525,"icon":105},"在线课程与教育平台","\u002Fzh-hans\u002Fusecases\u002Fonline-courses","zh-Hans\u002F4.usecases\u002F1.online-courses",{"title":527,"path":528,"stem":529,"icon":43},"视频平台与 UGC 内容","\u002Fzh-hans\u002Fusecases\u002Fvideo-platforms","zh-Hans\u002F4.usecases\u002F2.video-platforms",{"title":531,"path":532,"stem":533,"icon":114},"直播回放与赛事录像","\u002Fzh-hans\u002Fusecases\u002Flive-replays","zh-Hans\u002F4.usecases\u002F3.live-replays",{"title":535,"path":536,"stem":537,"icon":119},"播客与音频内容","\u002Fzh-hans\u002Fusecases\u002Fpodcasts-audio","zh-Hans\u002F4.usecases\u002F4.podcasts-audio",{"title":539,"path":540,"stem":541,"icon":53},"图库与壁纸批量下载","\u002Fzh-hans\u002Fusecases\u002Fimage-galleries","zh-Hans\u002F4.usecases\u002F5.image-galleries",{"title":543,"path":544,"stem":545,"icon":128},"企业培训与内部视频","\u002Fzh-hans\u002Fusecases\u002Fcorporate-training","zh-Hans\u002F4.usecases\u002F6.corporate-training",{"title":547,"path":548,"stem":549,"icon":133},"个人媒体收藏与归档","\u002Fzh-hans\u002Fusecases\u002Fpersonal-archiving","zh-Hans\u002F4.usecases\u002F7.personal-archiving",{"title":551,"icon":136,"path":552,"stem":553,"children":554,"page":32},"故障排除","\u002Fzh-hans\u002Ftroubleshooting","zh-Hans\u002F5.troubleshooting",[555,559,563],{"title":556,"path":557,"stem":558,"icon":136},"常见问题排查","\u002Fzh-hans\u002Ftroubleshooting\u002Fcommon-issues","zh-Hans\u002F5.troubleshooting\u002F1.common-issues",{"title":560,"path":561,"stem":562,"icon":148},"已知限制与注意事项","\u002Fzh-hans\u002Ftroubleshooting\u002Fknown-issues","zh-Hans\u002F5.troubleshooting\u002F2.known-issues",{"title":564,"path":565,"stem":566,"icon":153},"常见问题解答","\u002Fzh-hans\u002Ftroubleshooting\u002Ffaq","zh-Hans\u002F5.troubleshooting\u002F3.faq",{"title":568,"icon":156,"path":569,"stem":570,"children":571,"page":32},"开发者文档","\u002Fzh-hans\u002Fdeveloper","zh-Hans\u002F6.developer",[572,576],{"title":573,"path":574,"stem":575,"icon":164},"贡献指南","\u002Fzh-hans\u002Fdeveloper\u002Fcontributing","zh-Hans\u002F6.developer\u002F1.contributing",{"title":577,"path":578,"stem":579,"icon":169},"项目架构","\u002Fzh-hans\u002Fdeveloper\u002Farchitecture","zh-Hans\u002F6.developer\u002F2.architecture",{"title":581,"path":582,"stem":583,"children":584,"page":32},"Zh Hant","\u002Fzh-hant","zh-Hant",[585,603,636,652,684,701],{"title":586,"path":587,"stem":588,"children":589,"icon":32},"快速入門","\u002Fzh-hant\u002Fgetting-started","zh-Hant\u002F1.getting-started\u002F1.index",[590,592,596,599],{"title":591,"path":587,"stem":588,"icon":16},"簡介",{"title":593,"path":594,"stem":595,"icon":21},"安裝","\u002Fzh-hant\u002Fgetting-started\u002Finstallation","zh-Hant\u002F1.getting-started\u002F2.installation",{"title":187,"path":597,"stem":598,"icon":26},"\u002Fzh-hant\u002Fgetting-started\u002Fusage","zh-Hant\u002F1.getting-started\u002F3.usage",{"title":600,"path":601,"stem":602,"icon":31},"配置參考","\u002Fzh-hant\u002Fgetting-started\u002Fconfiguration","zh-Hant\u002F1.getting-started\u002F4.configuration",{"title":604,"icon":35,"path":605,"stem":606,"children":607,"page":32},"功能特色","\u002Fzh-hant\u002Ffeatures","zh-Hant\u002F2.features",[608,612,616,620,624,628,632],{"title":609,"path":610,"stem":611,"icon":43},"影片嗅探","\u002Fzh-hant\u002Ffeatures\u002Fvideo-sniffing","zh-Hant\u002F2.features\u002F1.video-sniffing",{"title":613,"path":614,"stem":615,"icon":48},"音訊擷取","\u002Fzh-hant\u002Ffeatures\u002Faudio-capture","zh-Hant\u002F2.features\u002F2.audio-capture",{"title":617,"path":618,"stem":619,"icon":53},"圖片下載","\u002Fzh-hant\u002Ffeatures\u002Fimage-download","zh-Hant\u002F2.features\u002F3.image-download",{"title":621,"path":622,"stem":623,"icon":58},"隱私與安全","\u002Fzh-hant\u002Ffeatures\u002Fprivacy-security","zh-Hant\u002F2.features\u002F4.privacy-security",{"title":625,"path":626,"stem":627,"icon":63},"格式轉換","\u002Fzh-hant\u002Ffeatures\u002Fformat-conversion","zh-Hant\u002F2.features\u002F5.format-conversion",{"title":629,"path":630,"stem":631,"icon":68},"批次下載","\u002Fzh-hant\u002Ffeatures\u002Fbatch-download","zh-Hant\u002F2.features\u002F6.batch-download",{"title":633,"path":634,"stem":635,"icon":73},"鍵盤快速鍵","\u002Fzh-hant\u002Ffeatures\u002Fkeyboard-shortcuts","zh-Hant\u002F2.features\u002F7.keyboard-shortcuts",{"title":501,"icon":76,"path":637,"stem":638,"children":639,"page":32},"\u002Fzh-hant\u002Fadvanced","zh-Hant\u002F3.advanced",[640,644,648],{"title":641,"path":642,"stem":643,"icon":84},"線上工具","\u002Fzh-hant\u002Fadvanced\u002Fonline-tools","zh-Hant\u002F3.advanced\u002F1.online-tools",{"title":645,"path":646,"stem":647,"icon":89},"瀏覽器相容性","\u002Fzh-hant\u002Fadvanced\u002Fbrowser-compatibility","zh-Hant\u002F3.advanced\u002F2.browser-compatibility",{"title":649,"path":650,"stem":651,"icon":94},"下載引擎架構","\u002Fzh-hant\u002Fadvanced\u002Fdownload-engine","zh-Hant\u002F3.advanced\u002F3.download-engine",{"title":518,"icon":97,"path":653,"stem":654,"children":655,"page":32},"\u002Fzh-hant\u002Fusecases","zh-Hant\u002F4.usecases",[656,660,664,668,672,676,680],{"title":657,"path":658,"stem":659,"icon":105},"線上課程與教育平台","\u002Fzh-hant\u002Fusecases\u002Fonline-courses","zh-Hant\u002F4.usecases\u002F1.online-courses",{"title":661,"path":662,"stem":663,"icon":43},"影片平台與 UGC 內容","\u002Fzh-hant\u002Fusecases\u002Fvideo-platforms","zh-Hant\u002F4.usecases\u002F2.video-platforms",{"title":665,"path":666,"stem":667,"icon":114},"直播回放與賽事錄影","\u002Fzh-hant\u002Fusecases\u002Flive-replays","zh-Hant\u002F4.usecases\u002F3.live-replays",{"title":669,"path":670,"stem":671,"icon":119},"播客與音訊內容","\u002Fzh-hant\u002Fusecases\u002Fpodcasts-audio","zh-Hant\u002F4.usecases\u002F4.podcasts-audio",{"title":673,"path":674,"stem":675,"icon":53},"圖庫與桌布批次下載","\u002Fzh-hant\u002Fusecases\u002Fimage-galleries","zh-Hant\u002F4.usecases\u002F5.image-galleries",{"title":677,"path":678,"stem":679,"icon":128},"企業培訓與內部影片","\u002Fzh-hant\u002Fusecases\u002Fcorporate-training","zh-Hant\u002F4.usecases\u002F6.corporate-training",{"title":681,"path":682,"stem":683,"icon":133},"個人媒體收藏與歸檔","\u002Fzh-hant\u002Fusecases\u002Fpersonal-archiving","zh-Hant\u002F4.usecases\u002F7.personal-archiving",{"title":685,"icon":136,"path":686,"stem":687,"children":688,"page":32},"疑難排解","\u002Fzh-hant\u002Ftroubleshooting","zh-Hant\u002F5.troubleshooting",[689,693,697],{"title":690,"path":691,"stem":692,"icon":136},"常見問題排查","\u002Fzh-hant\u002Ftroubleshooting\u002Fcommon-issues","zh-Hant\u002F5.troubleshooting\u002F1.common-issues",{"title":694,"path":695,"stem":696,"icon":148},"已知限制與注意事項","\u002Fzh-hant\u002Ftroubleshooting\u002Fknown-issues","zh-Hant\u002F5.troubleshooting\u002F2.known-issues",{"title":698,"path":699,"stem":700,"icon":153},"常見問題解答","\u002Fzh-hant\u002Ftroubleshooting\u002Ffaq","zh-Hant\u002F5.troubleshooting\u002F3.faq",{"title":568,"icon":156,"path":702,"stem":703,"children":704,"page":32},"\u002Fzh-hant\u002Fdeveloper","zh-Hant\u002F6.developer",[705,709],{"title":706,"path":707,"stem":708,"icon":164},"貢獻指南","\u002Fzh-hant\u002Fdeveloper\u002Fcontributing","zh-Hant\u002F6.developer\u002F1.contributing",{"title":710,"path":711,"stem":712,"icon":169},"專案架構","\u002Fzh-hant\u002Fdeveloper\u002Farchitecture","zh-Hant\u002F6.developer\u002F2.architecture",{"id":714,"title":715,"author":716,"body":717,"category":1884,"date":1885,"description":1886,"extension":1887,"image":1715,"meta":1888,"navigation":1889,"path":1890,"readingTime":1891,"seo":1892,"stem":1893,"tags":1894,"__hash__":1900},"blog\u002Fblog\u002Fhow-i-built-flowpick-in-three-months.md","The Code Is Open Source! How I Built FlowPick in Three Months","FlowPick Team",{"type":718,"value":719,"toc":1870},"minimark",[720,732,735,738,743,746,749,752,755,758,762,765,773,783,786,789,793,796,807,810,813,825,999,1002,1005,1008,1021,1024,1028,1031,1041,1048,1051,1062,1065,1076,1082,1096,1099,1104,1107,1110,1115,1118,1121,1125,1131,1138,1144,1147,1150,1153,1179,1193,1197,1200,1203,1209,1212,1231,1372,1378,1381,1387,1391,1397,1400,1403,1409,1422,1425,1428,1432,1435,1442,1445,1614,1617,1620,1623,1627,1630,1633,1636,1703,1706,1709,1716,1720,1723,1729,1735,1741,1745,1748,1753,1774,1779,1790,1813,1815,1819,1866],[721,722,723,724,731],"p",{},"Today I pushed the code to ",[725,726,730],"a",{"href":727,"rel":728},"https:\u002F\u002Fgithub.com\u002FFlowPick\u002Fflowpick",[729],"nofollow","GitHub",". The repo is public now.",[721,733,734],{},"I'm not writing this to celebrate. I mainly want to document the pitfalls I fell into over those three months — so I can look them up later when I forget, and as a reference for anyone with the same needs. This will get into some technical details, but I'll keep it conversational. This is a story, not a spec document.",[736,737],"hr",{},[739,740,742],"h2",{"id":741},"the-trigger-an-experience-that-bugged-me-for-months","The Trigger: An Experience That Bugged Me for Months",[721,744,745],{},"Late last year, I was following a series of technical courses on an online learning platform. The platform didn't allow downloads, but I had a habit of watching offline — no signal on the subway commute, unreliable coffee shop WiFi, and on the way home I'd want to replay a section without scrubbing through a progress bar.",[721,747,748],{},"I tried Video DownloadHelper. For HLS streams, it required installing CoApp. Fine, I installed CoApp. Then it threw an error on an encrypted course. I searched for a few online tools — the ones that worked were either speed-capped, crashed on large files, or routed through a server proxy, which I wasn't comfortable with.",[721,750,751],{},"youtube-dl could handle some of it, but yt-dlp didn't recognize that platform at all — either there was no extractor for it, or the cookies wouldn't pass through.",[721,753,754],{},"This dragged on for months. Then one weekend, with nothing better to do, a thought popped into my head: isn't this just \"parse M3U8, download segments, stitch files together\"? Could this be done inside a browser?",[721,756,757],{},"I opened MDN and started browsing. The Web Crypto API, File System Access API, and WebAssembly — all three had stabilized in recent years, and compatibility on Chrome was no longer an issue. So I started coding.",[739,759,761],{"id":760},"week-1-figuring-out-can-we-even-detect-it","Week 1: Figuring Out \"Can We Even Detect It?\"",[721,763,764],{},"The first problem any tool has to solve: how do you know what videos are on the page?",[721,766,767,768,772],{},"Browser extensions have an API called ",[769,770,771],"code",{},"webRequest"," that can listen to all network requests made by a tab — and I mean all requests, at the network layer, not the DOM layer. The moment a player sends out an M3U8 or MPD request, the extension can intercept it.",[774,775,780],"pre",{"className":776,"code":778,"language":779},[777],"language-text","Extension → webRequest.onBeforeRequest\n         → Filter by Content-Type\n         → Hit application\u002Fx-mpegurl → Record as HLS\n         → Hit application\u002Fdash+xml  → Record as DASH\n         → Hit video\u002F*               → Record as direct video\n","text",[769,781,778],{"__ignoreMap":782},"",[721,784,785],{},"This part went faster than I expected. Two days later, I had a prototype that could list all video streams on the current page in the extension popup. I opened Bilibili to test — boom, it listed video streams, audio streams, ad streams, everything, complete with URLs and file sizes.",[721,787,788],{},"One detail tripped me up for a while: M3U8 comes in two flavors — Master Playlist and Media Playlist. The Master is a directory listing all available quality levels, each pointing to a Media Playlist; the Media Playlist is the actual segment manifest. I initially displayed both, and users saw a mess of entries with no idea which to pick. I later changed it: when a Master Playlist is detected, automatically parse the quality options inside and only show a human-readable resolution list, hiding the raw URL hierarchy.",[739,790,792],{"id":791},"week-3-the-first-real-hard-problem-where-do-the-files-go","Week 3: The First Real Hard Problem — Where Do the Files Go?",[721,794,795],{},"Downloading hundreds of segments — that's an async concurrency problem, not a hard one. The hard part was where to put all that data.",[721,797,798,799,802,803,806],{},"Browser JavaScript has no default permission to access the filesystem. The simplest approach is to accumulate all the data into a Blob, then trigger a download — ",[769,800,801],{},"URL.createObjectURL(blob)"," paired with a hidden ",[769,804,805],{},"\u003Ca>"," tag.",[721,808,809],{},"I got the prototype running with this method first. 500MB video, no problem. 800MB, still fine. 1GB, Chrome started to stutter — the reason was obvious. Blob mode loads the entire file into memory. A 1GB video means the browser is holding 1GB of ArrayBuffer, plus the page and extension overhead. Memory pressure was severe. Above 1.5GB, it just froze.",[721,811,812],{},"I set a hard limit on Blob mode: refuse to process anything over 1.5GB, warn at 800MB. That wasn't solving the problem — it was just drawing a line around it.",[721,814,815,816,820,821,824],{},"The real solution was the ",[817,818,819],"strong",{},"File System Access API (FSA)",". Supported since Chrome 86, it lets JavaScript obtain a file handle and then continuously write through a ",[769,822,823],{},"WritableStream"," — just like a native program writing to a file. Only the current chunk being written is in memory at any time, completely independent of total file size.",[774,826,830],{"className":827,"code":828,"language":829,"meta":782,"style":782},"language-typescript shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","const writable = await handle.createWritable()\n\u002F\u002F Configured with a 16MB buffer\nconst stream = new WritableStream({\n  async write(chunk) { await writable.write(chunk) },\n  async close() { await writable.close() }\n}, { highWaterMark: 16 * 1024 * 1024 })\n","typescript",[769,831,832,866,873,895,937,965],{"__ignoreMap":782},[833,834,837,841,845,849,853,856,859,863],"span",{"class":835,"line":836},"line",1,[833,838,840],{"class":839},"spNyl","const",[833,842,844],{"class":843},"sTEyZ"," writable ",[833,846,848],{"class":847},"sMK4o","=",[833,850,852],{"class":851},"s7zQu"," await",[833,854,855],{"class":843}," handle",[833,857,858],{"class":847},".",[833,860,862],{"class":861},"s2Zo4","createWritable",[833,864,865],{"class":843},"()\n",[833,867,869],{"class":835,"line":868},2,[833,870,872],{"class":871},"sHwdD","\u002F\u002F Configured with a 16MB buffer\n",[833,874,876,878,881,883,886,889,892],{"class":835,"line":875},3,[833,877,840],{"class":839},[833,879,880],{"class":843}," stream ",[833,882,848],{"class":847},[833,884,885],{"class":847}," new",[833,887,888],{"class":861}," WritableStream",[833,890,891],{"class":843},"(",[833,893,894],{"class":847},"{\n",[833,896,898,901,905,907,911,914,917,919,922,924,927,929,931,934],{"class":835,"line":897},4,[833,899,900],{"class":839},"  async",[833,902,904],{"class":903},"swJcz"," write",[833,906,891],{"class":847},[833,908,910],{"class":909},"sHdIc","chunk",[833,912,913],{"class":847},")",[833,915,916],{"class":847}," {",[833,918,852],{"class":851},[833,920,921],{"class":843}," writable",[833,923,858],{"class":847},[833,925,926],{"class":861},"write",[833,928,891],{"class":903},[833,930,910],{"class":843},[833,932,933],{"class":903},") ",[833,935,936],{"class":847},"},\n",[833,938,940,942,945,948,950,952,954,956,959,962],{"class":835,"line":939},5,[833,941,900],{"class":839},[833,943,944],{"class":903}," close",[833,946,947],{"class":847},"()",[833,949,916],{"class":847},[833,951,852],{"class":851},[833,953,921],{"class":843},[833,955,858],{"class":847},[833,957,958],{"class":861},"close",[833,960,961],{"class":903},"() ",[833,963,964],{"class":847},"}\n",[833,966,968,971,973,976,979,983,986,989,991,993,996],{"class":835,"line":967},6,[833,969,970],{"class":847},"},",[833,972,916],{"class":847},[833,974,975],{"class":903}," highWaterMark",[833,977,978],{"class":847},":",[833,980,982],{"class":981},"sbssI"," 16",[833,984,985],{"class":847}," *",[833,987,988],{"class":981}," 1024",[833,990,985],{"class":847},[833,992,988],{"class":981},[833,994,995],{"class":847}," }",[833,997,998],{"class":843},")\n",[721,1000,1001],{},"With this, downloading a 5GB video and a 500MB video use essentially the same amount of memory.",[721,1003,1004],{},"But Firefox doesn't support the FSA API, and neither does Safari. I needed a fallback.",[721,1006,1007],{},"I ended up with a three-tier write strategy:",[1009,1010,1011,1015,1018],"ol",{},[1012,1013,1014],"li",{},"Prefer FSA API (Chrome\u002FEdge, no size limit)",[1012,1016,1017],{},"Fall back to StreamSaver.js (relies on Service Worker, better compatibility)",[1012,1019,1020],{},"Last resort Blob mode (all browsers, but 1.5GB limit)",[721,1022,1023],{},"Which tier gets used is determined at runtime. The user never needs to know what's happening behind the scenes.",[739,1025,1027],{"id":1026},"week-5-the-real-boss-fight-merging-segments-into-mp4","Week 5: The Real Boss Fight — Merging Segments into MP4",[721,1029,1030],{},"I could download segments. I could write files. But HLS segments are in TS format (MPEG Transport Stream), and most people want MP4.",[721,1032,1033,1034,1037,1038,1040],{},"If you just binary-concatenate 180 ",[769,1035,1036],{},".ts"," files end to end, you get a big ",[769,1039,1036],{}," file. VLC can play it, but Windows' native player won't recognize it, CapCut can't import it, and uploading to Bilibili throws a format error. It technically works, but the user experience is terrible.",[721,1042,1043,1044,1047],{},"TS to MP4 is fundamentally ",[817,1045,1046],{},"remuxing",": extract the video and audio streams from TS and repackage them into MP4's container structure. No re-encoding involved, so it should be fast in theory, with zero quality loss.",[721,1049,1050],{},"The question was: in a browser, who's going to do this?",[721,1052,1053,1054,1057,1058,1061],{},"The answer had existed for about a year already — ",[817,1055,1056],{},"FFmpeg had been compiled to WebAssembly",". Several open-source projects had done this, the most well-known being ",[769,1059,1060],{},"@ffmpeg\u002Fffmpeg",". WASM can run near-native-speed binary code inside the browser's JavaScript environment, and FFmpeg natively supports remuxing.",[721,1063,1064],{},"In theory, all I needed to do was:",[1009,1066,1067,1070,1073],{},[1012,1068,1069],{},"Load FFmpeg WASM",[1012,1071,1072],{},"Write segments into FFmpeg's virtual filesystem (it simulates a filesystem in memory)",[1012,1074,1075],{},"Run this command:",[774,1077,1080],{"className":1078,"code":1079,"language":779},[777],"ffmpeg -f concat -safe 0 -i filelist.txt \\\n  -c copy \\\n  -movflags +faststart \\\n  -bsf:a aac_adtstoasc \\\n  output.mp4\n",[769,1081,1079],{"__ignoreMap":782},[721,1083,1084,1087,1088,1091,1092,1095],{},[769,1085,1086],{},"-c copy"," is stream copy — no re-encoding, just a container swap. ",[769,1089,1090],{},"-bsf:a aac_adtstoasc"," converts AAC audio from the ADTS packaging used by TS to the ASC packaging used by MP4; without it, some players have audio issues. ",[769,1093,1094],{},"-movflags +faststart"," moves the MP4 index (moov atom) to the file start for streaming playback.",[721,1097,1098],{},"Sounded great. In practice, I hit several walls:",[721,1100,1101],{},[817,1102,1103],{},"Wall 1: FFmpeg WASM takes 10-30 seconds to load the first time.",[721,1105,1106],{},"The WASM core file is about 30MB, needing network download and compilation. The user clicks download, then waits 20 seconds with nothing happening — terrible UX.",[721,1108,1109],{},"Fix: trigger FFmpeg loading the moment the download starts, running it in parallel with segment downloads. By the time all segments are downloaded, FFmpeg is usually initialized, making the wait imperceptible. Plus, with browser caching, subsequent loads only take 2-5 seconds.",[721,1111,1112],{},[817,1113,1114],{},"Wall 2: The WASM virtual filesystem lives in memory.",[721,1116,1117],{},"FFmpeg processes files by first writing them into its virtual filesystem. A one-hour 1080P video — 600-800MB of segments — all written into WASM memory. Add FFmpeg's own runtime memory (200-400MB), and you easily exceed WASM's 4GB address space limit. Crash.",[721,1119,1120],{},"This wasn't an occasional issue with large files — it was inevitable. I needed a large-file solution that didn't depend on FFmpeg WASM.",[739,1122,1124],{"id":1123},"week-7-writing-tstomp4muxer","Week 7: Writing TSToMP4Muxer",[721,1126,1127,1128,858],{},"I spent nearly two weeks writing a pure-JavaScript TS → MP4 streaming remuxer called ",[769,1129,1130],{},"TSToMP4Muxer",[721,1132,1133,1134,1137],{},"The principle: instead of waiting for all segments to download before processing, ",[817,1135,1136],{},"download, parse, and write to disk simultaneously",". Each TS data packet is fixed at 188 bytes. I read packet by packet, extract PES (Packetized Elementary Stream) data, convert it to MP4 Box format in real time, and continuously write to disk via the Streams API.",[774,1139,1142],{"className":1140,"code":1141,"language":779},[777],"Segment 0 downloaded\n  → Parse 188-byte TS packets\n  → Extract video\u002Faudio PES\n  → Write to MP4 mdat Box\nSegment 1 downloaded\n  → Keep appending\n...\nSegment N written\n  → Write moov Box (index)\n  → Close file\n",[769,1143,1141],{"__ignoreMap":782},[721,1145,1146],{},"This way, only the currently-processing segment is in memory at any time — completely decoupled from total file size. And since it's pure JS, there's no need to load the FFmpeg WASM file, saving that 30MB load overhead and initialization time.",[721,1148,1149],{},"The trade-off is narrower coverage — it only does single TS → MP4. It can't handle DASH audio\u002Fvideo merging (which requires synchronized dual-input processing) or various edge-case formats. But for the most common scenario — HLS downloads — TSToMP4Muxer is the better solution.",[721,1151,1152],{},"The current logic: regular HLS prefers TSToMP4Muxer for streaming processing — fast and memory-efficient. DASH or dual-stream merging scenarios use FFmpeg WASM. HLS files over 2GB also go through TSToMP4Muxer to avoid WASM memory overflow.",[721,1154,1155,1156,1159,1160,1159,1163,1159,1166,1159,1169,1159,1172,1159,1175,1178],{},"Writing this took a long time. The MP4 Box structure has a spec — a 600+ page spec. I only read the few dozen pages relevant to remuxing. ",[769,1157,1158],{},"ftyp",", ",[769,1161,1162],{},"moov",[769,1164,1165],{},"mvhd",[769,1167,1168],{},"trak",[769,1170,1171],{},"mdia",[769,1173,1174],{},"minf",[769,1176,1177],{},"stbl"," — if the nesting hierarchy of these Boxes is wrong, players can't read the file. The debugging loop was: write it, open in VLC, see what error it throws, check the spec, fix, repeat. Probably cycled through this twenty-plus times.",[721,1180,1181,1182,1185,1186,1189,1190,1192],{},"The most maddening moment: the video played, but the timeline was scrambled — dragging the seek bar jumped to random positions, not where you expected. The cause: the timestamp calculations in the ",[769,1183,1184],{},"stts"," (Sample to Time) and ",[769,1187,1188],{},"ctts"," (Composition Time Offset) Boxes weren't handling B-frame DTS\u002FPTS offsets. H.264 has B-frames, where decode order (DTS) and display order (PTS) differ. The ",[769,1191,1188],{}," Box has to separately record each frame's display time offset. Found the problem, fixed it — took two days.",[739,1194,1196],{"id":1195},"week-9-aes-128-encryption-easier-than-i-thought","Week 9: AES-128 Encryption — Easier Than I Thought",[721,1198,1199],{},"I'd always assumed encrypted video handling would be the hardest part. Turns out it was way easier than TSToMP4Muxer.",[721,1201,1202],{},"HLS encryption is clearly specified in the M3U8:",[774,1204,1207],{"className":1205,"code":1206,"language":779},[777],"#EXT-X-KEY:METHOD=AES-128,URI=\"https:\u002F\u002Fapi.example.com\u002Fkey?token=abc\",IV=0x00000000000000000000000000000001\n",[769,1208,1206],{"__ignoreMap":782},[721,1210,1211],{},"The logic is:",[1009,1213,1214,1221,1224],{},[1012,1215,1216,1217,1220],{},"Spot ",[769,1218,1219],{},"#EXT-X-KEY",", parse out the Key URI and IV",[1012,1222,1223],{},"Request the Key URI to get the 16-byte key (the request comes from the browser extension, carrying the user's current cookies — so even keys that require login are accessible)",[1012,1225,1226,1227,1230],{},"For each downloaded segment, use the ",[769,1228,1229],{},"Web Crypto API"," for AES-128-CBC decryption",[774,1232,1234],{"className":827,"code":1233,"language":829,"meta":782,"style":782},"const keyBytes = await crypto.subtle.importKey('raw', key, { name: 'AES-CBC' }, false, ['decrypt'])\nconst decrypted = await crypto.subtle.decrypt({ name: 'AES-CBC', iv: ivBuffer }, keyBytes, encryptedData)\n",[769,1235,1236,1316],{"__ignoreMap":782},[833,1237,1238,1240,1243,1245,1247,1250,1252,1255,1257,1260,1262,1265,1269,1271,1274,1277,1279,1281,1284,1286,1289,1292,1294,1297,1301,1303,1306,1308,1311,1313],{"class":835,"line":836},[833,1239,840],{"class":839},[833,1241,1242],{"class":843}," keyBytes ",[833,1244,848],{"class":847},[833,1246,852],{"class":851},[833,1248,1249],{"class":843}," crypto",[833,1251,858],{"class":847},[833,1253,1254],{"class":843},"subtle",[833,1256,858],{"class":847},[833,1258,1259],{"class":861},"importKey",[833,1261,891],{"class":843},[833,1263,1264],{"class":847},"'",[833,1266,1268],{"class":1267},"sfazB","raw",[833,1270,1264],{"class":847},[833,1272,1273],{"class":847},",",[833,1275,1276],{"class":843}," key",[833,1278,1273],{"class":847},[833,1280,916],{"class":847},[833,1282,1283],{"class":903}," name",[833,1285,978],{"class":847},[833,1287,1288],{"class":847}," '",[833,1290,1291],{"class":1267},"AES-CBC",[833,1293,1264],{"class":847},[833,1295,1296],{"class":847}," },",[833,1298,1300],{"class":1299},"sfNiH"," false",[833,1302,1273],{"class":847},[833,1304,1305],{"class":843}," [",[833,1307,1264],{"class":847},[833,1309,1310],{"class":1267},"decrypt",[833,1312,1264],{"class":847},[833,1314,1315],{"class":843},"])\n",[833,1317,1318,1320,1323,1325,1327,1329,1331,1333,1335,1337,1339,1342,1344,1346,1348,1350,1352,1354,1357,1359,1362,1364,1367,1369],{"class":835,"line":868},[833,1319,840],{"class":839},[833,1321,1322],{"class":843}," decrypted ",[833,1324,848],{"class":847},[833,1326,852],{"class":851},[833,1328,1249],{"class":843},[833,1330,858],{"class":847},[833,1332,1254],{"class":843},[833,1334,858],{"class":847},[833,1336,1310],{"class":861},[833,1338,891],{"class":843},[833,1340,1341],{"class":847},"{",[833,1343,1283],{"class":903},[833,1345,978],{"class":847},[833,1347,1288],{"class":847},[833,1349,1291],{"class":1267},[833,1351,1264],{"class":847},[833,1353,1273],{"class":847},[833,1355,1356],{"class":903}," iv",[833,1358,978],{"class":847},[833,1360,1361],{"class":843}," ivBuffer ",[833,1363,970],{"class":847},[833,1365,1366],{"class":843}," keyBytes",[833,1368,1273],{"class":847},[833,1370,1371],{"class":843}," encryptedData)\n",[721,1373,1374,1377],{},[769,1375,1376],{},"crypto.subtle"," is the browser's built-in Web Crypto API, with hardware acceleration under the hood. Decryption speed isn't a bottleneck — a few milliseconds per segment.",[721,1379,1380],{},"What actually took time was IV handling. The HLS spec says: if the M3U8 doesn't explicitly declare an IV, each segment's IV should use the segment's sequence number (MSB format, 16 bytes). I initially didn't handle this rule and used the first IV for all segments. Encrypted courses decrypted with correct video but garbled audio. Looked up RFC 8216, found the rule, added it — fixed.",[721,1382,1383,1386],{},[817,1384,1385],{},"What can't be handled:"," Widevine, PlayReady, FairPlay. These are DRM. The keys live in the OS or hardware; JavaScript simply can't access them. When FlowPick encounters this type of content, it can only honestly tell the user \"not supported\" — there's no way around it. Netflix and Disney+ are in this category and were never in scope.",[739,1388,1390],{"id":1389},"week-11-dash-audiovideo-merging","Week 11: DASH Audio\u002FVideo Merging",[721,1392,1393,1394,858],{},"DASH is the other major streaming format, used by YouTube and regular Bilibili uploads. The core difference from HLS: ",[817,1395,1396],{},"audio and video are two completely independent streams",[721,1398,1399],{},"This isn't a design flaw — it's intentional. It lets platforms store one video track and pair it with multiple language audio tracks, slashing storage costs by several times. But for a download tool, it means you have to download two streams simultaneously and then merge them.",[721,1401,1402],{},"Merging two streams uses FFmpeg WASM:",[774,1404,1407],{"className":1405,"code":1406,"language":779},[777],"ffmpeg\n  -f concat -safe 0 -i video_list.txt\n  -f concat -safe 0 -i audio_list.txt\n  -c:v copy -c:a copy\n  -movflags +faststart\n  output.mp4\n",[769,1408,1406],{"__ignoreMap":782},[721,1410,1411,1412,1415,1416,1418,1419,1421],{},"One detail here: DASH segments are in FMP4 (Fragmented MP4) format, not HLS's TS. The special thing about FMP4 is that each segment file can play independently, but to parse them correctly, you must first have an ",[817,1413,1414],{},"init segment"," — an initialization fragment containing codec parameters (",[769,1417,1158],{}," + ",[769,1420,1162],{}," Box). Without the init segment, FFmpeg doesn't know the video's resolution, frame rate, or codec, and can't process the subsequent data segments.",[721,1423,1424],{},"So the DASH processing flow has one extra step compared to HLS: download the init segment first, then all data segments, and the init segment must come first during merging. I initially forgot to handle the init segment, and FFmpeg's merged MP4 opened with a \"file corrupted\" error. Took two days to realize the problem was here.",[721,1426,1427],{},"The DASH parallel download strategy also needed adjustment — video segments and audio segments should download simultaneously, not video first then audio. This nearly halved the total download time. The Worker Pool design supports this: two task streams in parallel, sharing one thread pool — whichever worker is free grabs the next segment from whichever stream.",[739,1429,1431],{"id":1430},"concurrent-downloads-worker-pool-not-pre-splitting","Concurrent Downloads: Worker Pool, Not Pre-Splitting",[721,1433,1434],{},"I want to call out the concurrent download design separately because one implementation detail made me rewrite it twice.",[721,1436,1437,1438,1441],{},"The most intuitive approach: split 200 segments into 4 equal batches, 4 Workers each handle one batch. The problem with this is ",[817,1439,1440],{},"load imbalance"," — HLS segments aren't equal-sized. Segments at the start and end of a video tend to be smaller, middle ones larger. Pre-splitting can leave one Worker stuck on a batch of large segments while the other three are already idle.",[721,1443,1444],{},"FlowPick uses a shared counter approach:",[774,1446,1448],{"className":827,"code":1447,"language":829,"meta":782,"style":782},"let nextIndex = 0\nconst worker = async () => {\n  while (nextIndex \u003C segments.length) {\n    const index = nextIndex++  \u002F\u002F Atomically grab the next task\n    await downloadAndProcess(segments[index])\n  }\n}\n\u002F\u002F Launch N workers, all sharing nextIndex\nawait Promise.all(Array.from({ length: concurrency }, () => worker()))\n",[769,1449,1450,1463,1484,1510,1530,1551,1556,1561,1567],{"__ignoreMap":782},[833,1451,1452,1455,1458,1460],{"class":835,"line":836},[833,1453,1454],{"class":839},"let",[833,1456,1457],{"class":843}," nextIndex ",[833,1459,848],{"class":847},[833,1461,1462],{"class":981}," 0\n",[833,1464,1465,1467,1470,1472,1475,1478,1481],{"class":835,"line":868},[833,1466,840],{"class":839},[833,1468,1469],{"class":843}," worker ",[833,1471,848],{"class":847},[833,1473,1474],{"class":839}," async",[833,1476,1477],{"class":847}," ()",[833,1479,1480],{"class":839}," =>",[833,1482,1483],{"class":847}," {\n",[833,1485,1486,1489,1492,1495,1498,1501,1503,1506,1508],{"class":835,"line":875},[833,1487,1488],{"class":851},"  while",[833,1490,1491],{"class":903}," (",[833,1493,1494],{"class":843},"nextIndex",[833,1496,1497],{"class":847}," \u003C",[833,1499,1500],{"class":843}," segments",[833,1502,858],{"class":847},[833,1504,1505],{"class":843},"length",[833,1507,933],{"class":903},[833,1509,894],{"class":847},[833,1511,1512,1515,1518,1521,1524,1527],{"class":835,"line":897},[833,1513,1514],{"class":839},"    const",[833,1516,1517],{"class":843}," index",[833,1519,1520],{"class":847}," =",[833,1522,1523],{"class":843}," nextIndex",[833,1525,1526],{"class":847},"++",[833,1528,1529],{"class":871},"  \u002F\u002F Atomically grab the next task\n",[833,1531,1532,1535,1538,1540,1543,1546,1549],{"class":835,"line":939},[833,1533,1534],{"class":851},"    await",[833,1536,1537],{"class":861}," downloadAndProcess",[833,1539,891],{"class":903},[833,1541,1542],{"class":843},"segments",[833,1544,1545],{"class":903},"[",[833,1547,1548],{"class":843},"index",[833,1550,1315],{"class":903},[833,1552,1553],{"class":835,"line":967},[833,1554,1555],{"class":847},"  }\n",[833,1557,1559],{"class":835,"line":1558},7,[833,1560,964],{"class":847},[833,1562,1564],{"class":835,"line":1563},8,[833,1565,1566],{"class":871},"\u002F\u002F Launch N workers, all sharing nextIndex\n",[833,1568,1570,1573,1577,1579,1582,1585,1587,1590,1592,1594,1597,1599,1602,1604,1606,1608,1611],{"class":835,"line":1569},9,[833,1571,1572],{"class":851},"await",[833,1574,1576],{"class":1575},"sBMFI"," Promise",[833,1578,858],{"class":847},[833,1580,1581],{"class":861},"all",[833,1583,1584],{"class":843},"(Array",[833,1586,858],{"class":847},[833,1588,1589],{"class":861},"from",[833,1591,891],{"class":843},[833,1593,1341],{"class":847},[833,1595,1596],{"class":903}," length",[833,1598,978],{"class":847},[833,1600,1601],{"class":843}," concurrency ",[833,1603,970],{"class":847},[833,1605,1477],{"class":847},[833,1607,1480],{"class":839},[833,1609,1610],{"class":861}," worker",[833,1612,1613],{"class":843},"()))\n",[721,1615,1616],{},"Each Worker grabs the next segment the moment it finishes the current one — no waiting for other Workers, regardless of segment size. This naturally achieves dynamic load balancing, with total completion time approaching the theoretical optimum.",[721,1618,1619],{},"The default concurrency is 2, adjustable up to 8. Why not default to 8? Because for some CDNs, high concurrency triggers rate limiting or 429 errors, making things slower. 4-6 concurrent threads is usually the sweet spot; diminishing returns kick in around 8.",[721,1621,1622],{},"The retry mechanism uses exponential backoff: wait 400ms after a failure, then 800ms, then 1600ms. Give up after 3 total failures. Most network hiccups recover on the first or second retry. 4xx errors (403, 404) are not retried — the server explicitly refused, retrying is pointless.",[739,1624,1626],{"id":1625},"memory-management-preventing-browser-crashes","Memory Management: Preventing Browser Crashes",[721,1628,1629],{},"In the final stretch, memory management was what I obsessed over the most.",[721,1631,1632],{},"The problem: you don't know how big the downloaded file will be. A user picks 1080P for a livestream replay — could be 2GB, could be 20GB. You won't know until all segments are downloaded.",[721,1634,1635],{},"I built a sampling-based estimate before downloading:",[774,1637,1639],{"className":827,"code":1638,"language":829,"meta":782,"style":782},"\u002F\u002F Download max(1, total × 10%) segments first, take the average size, multiply by total\nconst sampleCount = Math.min(5, Math.max(1, Math.floor(totalCount * 0.1)))\n",[769,1640,1641,1646],{"__ignoreMap":782},[833,1642,1643],{"class":835,"line":836},[833,1644,1645],{"class":871},"\u002F\u002F Download max(1, total × 10%) segments first, take the average size, multiply by total\n",[833,1647,1648,1650,1653,1655,1658,1660,1663,1665,1668,1670,1672,1674,1677,1679,1682,1684,1686,1688,1691,1694,1697,1700],{"class":835,"line":868},[833,1649,840],{"class":839},[833,1651,1652],{"class":843}," sampleCount ",[833,1654,848],{"class":847},[833,1656,1657],{"class":843}," Math",[833,1659,858],{"class":847},[833,1661,1662],{"class":861},"min",[833,1664,891],{"class":843},[833,1666,1667],{"class":981},"5",[833,1669,1273],{"class":847},[833,1671,1657],{"class":843},[833,1673,858],{"class":847},[833,1675,1676],{"class":861},"max",[833,1678,891],{"class":843},[833,1680,1681],{"class":981},"1",[833,1683,1273],{"class":847},[833,1685,1657],{"class":843},[833,1687,858],{"class":847},[833,1689,1690],{"class":861},"floor",[833,1692,1693],{"class":843},"(totalCount ",[833,1695,1696],{"class":847},"*",[833,1698,1699],{"class":981}," 0.1",[833,1701,1702],{"class":843},")))\n",[721,1704,1705],{},"Based on the estimate, pick a write strategy: under 800MB, anything goes. 800MB-1.5GB, must use streaming writes. Over 1.5GB, refuse Blob mode.",[721,1707,1708],{},"But estimates aren't enough — runtime monitoring is also needed. During download, continuously track total allocated ArrayBuffer size. Over 600MB triggers a warning. Over 1200MB pauses new segment downloads, prioritizes writing already-downloaded segments to disk and freeing memory, then resumes. This way, even if the estimate is way off, there's a safety net.",[721,1710,1711],{},[1712,1713],"img",{"alt":1714,"src":1715},"Download engine architecture: three core modules — segment download, segment processing, file writing","\u002Fscreenshots\u002Fdownload-engine-architecture.png",[739,1717,1719],{"id":1718},"features-i-built-and-then-deleted","Features I Built and Then Deleted",[721,1721,1722],{},"Over three months, a few features got halfway built and then scrapped:",[721,1724,1725,1728],{},[817,1726,1727],{},"Transcoding",": I briefly wanted to support \"convert to 720P on download\" to reduce file size. Then I realized transcoding in a browser is way too slow — FFmpeg WASM can handle remuxing fine, but re-encoding means decoding every video frame and re-encoding, CPU usage spikes to max, and a 1GB video could take 30 minutes. Unacceptable user experience. FlowPick ultimately only does remuxing (container swap, no codec change). If you want transcoding, use command-line FFmpeg.",[721,1730,1731,1734],{},[817,1732,1733],{},"Real-time ETA on the progress bar",": my first version used instantaneous speed, and the progress bar jittered wildly — CDN segment sizes aren't uniform, so instantaneous speed could bounce between 0.5MB\u002Fs and 50MB\u002Fs. Switched to a 5-second sliding window average, much smoother.",[721,1736,1737,1740],{},[817,1738,1739],{},"Multi-file batch downloading",": I originally wasn't going to do this, then saw people asking if they could download all episodes at once. Added a simple queue — each download task runs sequentially. No parallel support (two simultaneous video downloads would max out both bandwidth and FFmpeg memory).",[739,1742,1744],{"id":1743},"the-result-after-three-months","The Result After Three Months",[721,1746,1747],{},"About 12,000 lines of TypeScript — extension + online tool + landing page.",[721,1749,1750],{},[817,1751,1752],{},"What it can do:",[1754,1755,1756,1759,1762,1765,1768,1771],"ul",{},[1012,1757,1758],{},"HLS\u002FM3U8 downloads with automatic AES-128 decryption",[1012,1760,1761],{},"DASH\u002FMPD downloads with automatic audio\u002Fvideo merging",[1012,1763,1764],{},"Batch direct video sniffing",[1012,1766,1767],{},"Batch image detection and downloading",[1012,1769,1770],{},"Audio resource detection",[1012,1772,1773],{},"MP4 output, no file size limit (Chrome\u002FEdge)",[721,1775,1776],{},[817,1777,1778],{},"What it can't do (not laziness — genuinely impossible):",[1754,1780,1781,1784,1787],{},[1012,1782,1783],{},"Widevine\u002FPlayReady\u002FFairPlay DRM — hardware-level protection, JS can't access keys",[1012,1785,1786],{},"Video transcoding — browser compute power isn't enough, experience would be terrible",[1012,1788,1789],{},"Format conversion to MKV\u002FAVI — FFmpeg WASM integration currently only covers the MP4\u002FTS path",[721,1791,1792,1793,1796,1797,1159,1802,1159,1807,1812],{},"The code is open source, repo at ",[725,1794,730],{"href":727,"rel":1795},[729],". The extension is published on three browsers: ",[725,1798,1801],{"href":1799,"rel":1800},"https:\u002F\u002Fchromewebstore.google.com\u002Fdetail\u002Fflowpick-media-sniffer-do\u002Fmfinfkkabangbkanlfhhbokgfekjklea",[729],"Chrome Web Store",[725,1803,1806],{"href":1804,"rel":1805},"https:\u002F\u002Fmicrosoftedge.microsoft.com\u002Faddons\u002Fdetail\u002Fflowpick-media-sniffer-\u002Fegbfhgcifljmeaomlbhggplohemjombb",[729],"Edge Add-ons",[725,1808,1811],{"href":1809,"rel":1810},"https:\u002F\u002Faddons.mozilla.org\u002Ffirefox\u002Faddon\u002Fflowpick\u002F",[729],"Firefox Add-ons",". If you find bugs or have ideas, feel free to open an Issue or send a PR.",[736,1814],{},[739,1816,1818],{"id":1817},"recommended-reading","Recommended Reading",[1754,1820,1821,1830,1839,1848,1857],{},[1012,1822,1823,1829],{},[817,1824,1825],{},[725,1826,1828],{"href":1827},"\u002Fblog\u002Fhow-flowpick-merges-video-segments-in-browser","How FlowPick Merges Video Segments into MP4 in the Browser"," — this one tells the story, that one explains the principles. Read them together.",[1012,1831,1832,1838],{},[817,1833,1834],{},[725,1835,1837],{"href":1836},"\u002Fblog\u002Fgetting-started-with-hls-m3u8-download","How to Download M3U8\u002FHLS Streams: A Complete Beginner's Guide"," — after all those pitfalls, the user only sees five steps.",[1012,1840,1841,1847],{},[817,1842,1843],{},[725,1844,1846],{"href":1845},"\u002Fblog\u002Fwhat-is-dash-mpd-streaming","What Is DASH Streaming? MPD File Explained"," — the DASH format mentioned repeatedly throughout this article, explained in detail.",[1012,1849,1850,1856],{},[817,1851,1852],{},[725,1853,1855],{"href":1854},"\u002Fblog\u002Fhow-to-download-bilibili-videos","How to Download Bilibili Videos to Your Computer"," — DASH audio\u002Fvideo merging in a classic real-world case.",[1012,1858,1859,1865],{},[817,1860,1861],{},[725,1862,1864],{"href":1863},"\u002Fblog\u002Fflowpick-vs-video-downloadhelper","FlowPick vs. Video DownloadHelper"," — a head-to-head comparison with the oldest extension in this space.",[1867,1868,1869],"style",{},"html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}",{"title":782,"searchDepth":868,"depth":868,"links":1871},[1872,1873,1874,1875,1876,1877,1878,1879,1880,1881,1882,1883],{"id":741,"depth":868,"text":742},{"id":760,"depth":868,"text":761},{"id":791,"depth":868,"text":792},{"id":1026,"depth":868,"text":1027},{"id":1123,"depth":868,"text":1124},{"id":1195,"depth":868,"text":1196},{"id":1389,"depth":868,"text":1390},{"id":1430,"depth":868,"text":1431},{"id":1625,"depth":868,"text":1626},{"id":1718,"depth":868,"text":1719},{"id":1743,"depth":868,"text":1744},{"id":1817,"depth":868,"text":1818},"stories","2026-07-02","A solo developer who got fed up and built his own tool — the full journey from discovering the problem to open-source release: webRequest sniffing, FFmpeg WASM pitfalls, building TSToMP4Muxer from scratch, and the three-tier write strategy.","md",{},true,"\u002Fblog\u002Fhow-i-built-flowpick-in-three-months",16,{"title":715,"description":1886},"blog\u002Fhow-i-built-flowpick-in-three-months",[1895,1896,1897,1898,1899],"open source","solo dev","wasm","architecture","dev log","8U_ykhCHttGxgrWBQIlM84BHrYlrh75E078uBuZ_5Tg",[1902,1907],{"title":1903,"path":1827,"stem":1904,"description":1905,"date":1885,"category":1906,"children":-1},"How FlowPick Merges Hundreds of Video Segments into MP4 in the Browser","blog\u002Fhow-flowpick-merges-video-segments-in-browser","No server, no FFmpeg install, no transcoding — a full technical breakdown of how FlowPick handles HLS\u002FDASH segment merging entirely within a browser tab.","tips",{"title":1908,"path":1854,"stem":1909,"description":1910,"date":1911,"category":1912,"children":-1},"How to Download Bilibili Videos to Your Computer (2026 Latest Guide)","blog\u002Fhow-to-download-bilibili-videos","A complete tutorial on downloading Bilibili videos using the FlowPick browser extension, supporting HLS livestreams, DASH videos, and regular MP4 content.","2026-07-01","tutorials",1783158275028]