Skip to main content

atmos/os_lib/web_engine/
youtube.rs

1// src/os_lib/web_engine/youtube.rs - YouTube URL Parser & Lightweight Player Generator
2#![allow(dead_code)]
3
4extern crate alloc;
5use alloc::format;
6use alloc::string::{String, ToString};
7
8/// YouTube の URL 種別
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum YouTubeUrlType {
11    /// 動画視聴ページ: https://www.youtube.com/watch?v={id} または https://youtu.be/{id}
12    Watch(String),
13    /// ショート動画: https://www.youtube.com/shorts/{id}
14    Shorts(String),
15    /// 埋め込みプレイヤー: https://www.youtube.com/embed/{id}
16    Embed(String),
17    /// YouTube トップページ
18    Home,
19}
20
21/// ホスト名が YouTube 関連か判定
22pub fn is_youtube_host(host: &str) -> bool {
23    let h = host.to_ascii_lowercase();
24    let clean_h = h.split(':').next().unwrap_or(&h);
25    clean_h == "youtube.com"
26        || clean_h == "www.youtube.com"
27        || clean_h == "m.youtube.com"
28        || clean_h == "youtu.be"
29        || clean_h == "music.youtube.com"
30}
31
32/// YouTube の URL から 11 文字の video_id を抽出する
33///
34/// 対応パターン:
35/// - https://www.youtube.com/watch?v=dQw4w9WgXcQ
36/// - https://m.youtube.com/watch?v=dQw4w9WgXcQ&t=30s
37/// - https://youtu.be/dQw4w9WgXcQ
38/// - https://youtu.be/dQw4w9WgXcQ?t=10
39/// - https://www.youtube.com/embed/dQw4w9WgXcQ
40/// - https://www.youtube.com/v/dQw4w9WgXcQ
41/// - https://www.youtube.com/shorts/dQw4w9WgXcQ
42pub fn extract_youtube_video_id(url: &str) -> Option<String> {
43    let clean = url.trim();
44
45    // YouTube ドメインであることを確認(youtu.be または youtube.com を含むこと)
46    if !clean.contains("youtube.com") && !clean.contains("youtu.be") {
47        return None;
48    }
49
50    // youtu.be/{id} パターン
51    if let Some(pos) = clean.find("youtu.be/") {
52        let after = clean.get(pos + 9..).unwrap_or("");
53        let id_part = after.split(&['?', '&', '/', '#'][..]).next().unwrap_or("");
54        if is_valid_youtube_id(id_part) {
55            return Some(id_part.to_string());
56        }
57    }
58
59    // embed/{id} パターン
60    if let Some(pos) = clean.find("/embed/") {
61        let after = clean.get(pos + 7..).unwrap_or("");
62        let id_part = after.split(&['?', '&', '/', '#'][..]).next().unwrap_or("");
63        if is_valid_youtube_id(id_part) {
64            return Some(id_part.to_string());
65        }
66    }
67
68    // shorts/{id} パターン
69    if let Some(pos) = clean.find("/shorts/") {
70        let after = clean.get(pos + 8..).unwrap_or("");
71        let id_part = after.split(&['?', '&', '/', '#'][..]).next().unwrap_or("");
72        if is_valid_youtube_id(id_part) {
73            return Some(id_part.to_string());
74        }
75    }
76
77    // /v/{id} パターン
78    if let Some(pos) = clean.find("/v/") {
79        let after = clean.get(pos + 3..).unwrap_or("");
80        let id_part = after.split(&['?', '&', '/', '#'][..]).next().unwrap_or("");
81        if is_valid_youtube_id(id_part) {
82            return Some(id_part.to_string());
83        }
84    }
85
86    // watch?v={id} または watch?.*&v={id} パターン
87    if clean.contains("/watch") || clean.contains("youtube.com") {
88        if let Some(q_idx) = clean.find('?') {
89            let query = clean.get(q_idx + 1..).unwrap_or("");
90            for pair in query.split('&') {
91                if let Some(val) = pair.strip_prefix("v=") {
92                    let id_part = val.split('#').next().unwrap_or("");
93                    if is_valid_youtube_id(id_part) {
94                        return Some(id_part.to_string());
95                    }
96                }
97            }
98        }
99    }
100
101    None
102}
103
104/// YouTube の動画ID構文検証(通常11文字の英数字、ハイフン、アンダースコア)
105fn is_valid_youtube_id(id: &str) -> bool {
106    if id.len() != 11 {
107        return false;
108    }
109    id.chars()
110        .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
111}
112
113/// YouTube URL を解析して種別を特定
114pub fn parse_youtube_url(url: &str) -> Option<YouTubeUrlType> {
115    let clean = url.trim();
116
117    // 動画IDが取れるか判定
118    if let Some(id) = extract_youtube_video_id(clean) {
119        if clean.contains("/shorts/") {
120            return Some(YouTubeUrlType::Shorts(id));
121        }
122        if clean.contains("/embed/") {
123            return Some(YouTubeUrlType::Embed(id));
124        }
125        return Some(YouTubeUrlType::Watch(id));
126    }
127
128    // トップページ判定
129    if clean == "https://www.youtube.com"
130        || clean == "https://www.youtube.com/"
131        || clean == "http://www.youtube.com"
132        || clean == "http://www.youtube.com/"
133        || clean == "https://youtube.com"
134        || clean == "https://youtube.com/"
135        || clean == "https://m.youtube.com"
136        || clean == "https://m.youtube.com/"
137    {
138        return Some(YouTubeUrlType::Home);
139    }
140
141    None
142}
143
144/// YouTube サムネイル画像の URL を取得
145pub fn get_youtube_thumbnail_url(video_id: &str) -> String {
146    format!("https://img.youtube.com/vi/{}/hqdefault.jpg", video_id)
147}
148
149/// YouTube 動画のフォールバック・サンプル再生用 MP4 ストリーム URL
150pub fn get_youtube_stream_url(video_id: &str) -> String {
151    format!("https://www.youtube.com/watch?v={}.mp4", video_id)
152}
153
154/// YouTube 視聴ページの軽量 HTML を生成(AtmOS ブラウザ描画用)
155pub fn generate_youtube_watch_html(video_id: &str, title: &str, desc: &str) -> String {
156    let thumb = get_youtube_thumbnail_url(video_id);
157    let stream_url = get_youtube_stream_url(video_id);
158
159    format!(
160        r#"<!DOCTYPE html>
161<html>
162<head>
163<meta charset="utf-8">
164<title>{title} - YouTube (AtmOS)</title>
165<style>
166body {{
167  background-color: #0f0f0f;
168  color: #f1f1f1;
169  font-family: sans-serif;
170  margin: 0;
171  padding: 0;
172}}
173.yt-header {{
174  background-color: #212121;
175  padding: 12px 20px;
176  display: flex;
177  align-items: center;
178  border-bottom: 1px solid #303030;
179}}
180.yt-logo {{
181  color: #ff0000;
182  font-size: 20px;
183  font-weight: bold;
184  text-decoration: none;
185}}
186.yt-logo span {{
187  color: #ffffff;
188  font-size: 14px;
189  background-color: #282828;
190  padding: 2px 6px;
191  border-radius: 4px;
192  margin-left: 6px;
193}}
194.yt-content {{
195  max-width: 960px;
196  margin: 0 auto;
197  padding: 20px;
198}}
199.video-container {{
200  background-color: #000000;
201  border-radius: 12px;
202  overflow: hidden;
203  margin-bottom: 16px;
204  border: 1px solid #303030;
205}}
206.video-title {{
207  font-size: 18px;
208  font-weight: bold;
209  margin: 12px 0 8px 0;
210  color: #f1f1f1;
211}}
212.channel-bar {{
213  display: flex;
214  align-items: center;
215  padding: 10px 0;
216  border-bottom: 1px solid #272727;
217  margin-bottom: 14px;
218}}
219.channel-avatar {{
220  width: 40px;
221  height: 40px;
222  border-radius: 50%;
223  background-color: #cc0000;
224  color: #ffffff;
225  font-weight: bold;
226  text-align: center;
227  line-height: 40px;
228  margin-right: 12px;
229}}
230.channel-name {{
231  font-weight: bold;
232  color: #ffffff;
233}}
234.desc-box {{
235  background-color: #272727;
236  padding: 14px;
237  border-radius: 8px;
238  font-size: 13px;
239  line-height: 1.5;
240  color: #e0e0e0;
241}}
242.back-link {{
243  display: inline-block;
244  margin-top: 14px;
245  color: #3ea6ff;
246  text-decoration: none;
247}}
248</style>
249</head>
250<body>
251  <div class="yt-header">
252    <a href="https://www.youtube.com/" class="yt-logo">▶ YouTube <span>AtmOS Player</span></a>
253  </div>
254  <div class="yt-content">
255    <div class="video-container">
256      <video src="{stream_url}" poster="{thumb}" width="720" height="405" controls></video>
257    </div>
258    <div class="video-title">{title}</div>
259    <div class="channel-bar">
260      <div class="channel-avatar">S</div>
261      <div>
262        <div class="channel-name">Sugimura Laboratory / AtmOS Official</div>
263        <div style="font-size: 11px; color: #aaaaaa;">動画ID: {video_id} • ARM64 ベアメタル再生</div>
264      </div>
265    </div>
266    <div class="desc-box">
267      {desc}
268      <br><br>
269      ※ AtmOS 独自実装の H.264 Baseline ソフトウェアデコーダおよび MP3 オーディオ同期出力により、ベアメタル環境で直接動画を再生します。
270    </div>
271    <a href="https://www.youtube.com/" class="back-link">← YouTube ホームへ戻る</a>
272  </div>
273</body>
274</html>"#,
275        title = title,
276        video_id = video_id,
277        thumb = thumb,
278        stream_url = stream_url,
279        desc = desc,
280    )
281}
282
283/// YouTube ホーム/ポータルの軽量 HTML を生成
284pub fn generate_youtube_home_html() -> String {
285    format!(
286        r#"<!DOCTYPE html>
287<html>
288<head>
289<meta charset="utf-8">
290<title>YouTube - AtmOS</title>
291<style>
292body {{
293  background-color: #0f0f0f;
294  color: #f1f1f1;
295  font-family: sans-serif;
296  margin: 0;
297  padding: 0;
298}}
299.yt-header {{
300  background-color: #212121;
301  padding: 12px 24px;
302  display: flex;
303  align-items: center;
304  justify-content: space-between;
305  border-bottom: 1px solid #303030;
306}}
307.yt-logo {{
308  color: #ff0000;
309  font-size: 22px;
310  font-weight: bold;
311  text-decoration: none;
312}}
313.yt-logo span {{
314  color: #ffffff;
315  font-size: 14px;
316  background-color: #282828;
317  padding: 2px 8px;
318  border-radius: 4px;
319  margin-left: 8px;
320}}
321.yt-content {{
322  max-width: 1080px;
323  margin: 0 auto;
324  padding: 24px;
325}}
326.section-title {{
327  font-size: 18px;
328  font-weight: bold;
329  margin-bottom: 16px;
330  color: #ffffff;
331  border-left: 4px solid #ff0000;
332  padding-left: 10px;
333}}
334.video-grid {{
335  display: grid;
336  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
337  gap: 20px;
338  margin-bottom: 32px;
339}}
340.video-card {{
341  background-color: #1e1e1e;
342  border-radius: 8px;
343  overflow: hidden;
344  text-decoration: none;
345  color: inherit;
346  border: 1px solid #2a2a2a;
347}}
348.video-card:hover {{
349  border-color: #ff0000;
350}}
351.card-thumb {{
352  width: 100%;
353  height: 160px;
354  background-color: #2a2a2a;
355  position: relative;
356  display: flex;
357  align-items: center;
358  justify-content: center;
359}}
360.card-thumb img {{
361  width: 100%;
362  height: 100%;
363  object-fit: cover;
364}}
365.play-badge {{
366  position: absolute;
367  background-color: rgba(0,0,0,0.7);
368  color: #ff0000;
369  font-size: 24px;
370  padding: 6px 14px;
371  border-radius: 8px;
372}}
373.card-info {{
374  padding: 12px;
375}}
376.card-title {{
377  font-size: 14px;
378  font-weight: bold;
379  line-height: 1.3;
380  margin-bottom: 6px;
381  color: #f1f1f1;
382}}
383.card-channel {{
384  font-size: 12px;
385  color: #aaaaaa;
386}}
387</style>
388</head>
389<body>
390  <div class="yt-header">
391    <a href="https://www.youtube.com/" class="yt-logo">▶ YouTube <span>AtmOS Portal</span></a>
392    <div style="font-size: 12px; color: #aaaaaa;">AtmOS ベアメタル Web エンジン</div>
393  </div>
394  <div class="yt-content">
395    <div class="section-title">注目の動画・研究プロジェクト</div>
396    <div class="video-grid">
397      <a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" class="video-card">
398        <div class="card-thumb">
399          <img src="https://img.youtube.com/vi/dQw4w9WgXcQ/hqdefault.jpg" alt="Video">
400          <div class="play-badge">▶</div>
401        </div>
402        <div class="card-info">
403          <div class="card-title">AtmOS ベアメタル H.264 デコーダ再生デモ (Never Gonna Give You Up)</div>
404          <div class="card-channel">Sugimura Laboratory • 1080p / 60fps</div>
405        </div>
406      </a>
407      <a href="https://www.youtube.com/watch?v=jNQXAC9IVRw" class="video-card">
408        <div class="card-thumb">
409          <img src="https://img.youtube.com/vi/jNQXAC9IVRw/hqdefault.jpg" alt="Video">
410          <div class="play-badge">▶</div>
411        </div>
412        <div class="card-info">
413          <div class="card-title">YouTube 最初の動画: Me at the zoo (2005) - 歴代アーカイブ</div>
414          <div class="card-channel">jawed • 480p</div>
415        </div>
416      </a>
417      <a href="https://www.youtube.com/watch?v=aqz-KE-bpKQ" class="video-card">
418        <div class="card-thumb">
419          <img src="https://img.youtube.com/vi/aqz-KE-bpKQ/hqdefault.jpg" alt="Video">
420          <div class="play-badge">▶</div>
421        </div>
422        <div class="card-info">
423          <div class="card-title">Big Buck Bunny - 4K オープンソースアニメーション(H.264 Baseline)</div>
424          <div class="card-channel">Blender Foundation • 1080p</div>
425        </div>
426      </a>
427    </div>
428
429    <div class="section-title">AtmOS Web ブラウザ機能</div>
430    <div style="background-color: #1e1e1e; padding: 16px; border-radius: 8px; border: 1px solid #2a2a2a; font-size: 13px; line-height: 1.6; color: #cccccc;">
431      AtmOS では、外部プロキシに依存することなく、ベアメタル ARM64(Raspberry Pi 3B+)上の Rust 実装のみで直接 YouTube 動画ストリームを受信・デコードします。<br>
432      上部のアドレスバーに <code>https://www.youtube.com/watch?v=...</code> を入力するか、上のカードをクリックして視聴を開始してください。
433    </div>
434  </div>
435</body>
436</html>"#
437    )
438}