Add new audio file: nhac-phat-giao.mp3 to media directory
This commit is contained in:
@@ -1043,6 +1043,8 @@ function TourExperience({
|
||||
const narrationTokenRef = useRef(0);
|
||||
const activeNarrationSrcRef = useRef<string | null>(null);
|
||||
const lastNarrationSceneRef = useRef<SceneId | null>(null);
|
||||
// Listener "ended" của bài đang phát — giữ lại để gỡ sạch khi dừng/đổi hàng đợi.
|
||||
const narrationEndedRef = useRef<(() => void) | null>(null);
|
||||
|
||||
const { orientation, isSupported, requestPermission, startListening } = useDeviceOrientation();
|
||||
|
||||
@@ -1053,6 +1055,10 @@ function TourExperience({
|
||||
activeNarrationSrcRef.current = null;
|
||||
|
||||
if (audio) {
|
||||
if (narrationEndedRef.current) {
|
||||
audio.removeEventListener("ended", narrationEndedRef.current);
|
||||
narrationEndedRef.current = null;
|
||||
}
|
||||
audio.pause();
|
||||
audio.currentTime = 0;
|
||||
audio.onended = null;
|
||||
@@ -1074,6 +1080,12 @@ function TourExperience({
|
||||
const token = narrationTokenRef.current;
|
||||
narrationQueueRef.current = [...sources];
|
||||
|
||||
// Gỡ listener "ended" của lần phát trước (nếu còn) để không bị gọi chồng.
|
||||
if (narrationEndedRef.current) {
|
||||
audio.removeEventListener("ended", narrationEndedRef.current);
|
||||
narrationEndedRef.current = null;
|
||||
}
|
||||
|
||||
const playNext = async () => {
|
||||
if (token !== narrationTokenRef.current) {
|
||||
return;
|
||||
@@ -1087,17 +1099,28 @@ function TourExperience({
|
||||
return;
|
||||
}
|
||||
|
||||
if (audio.src !== nextSource) {
|
||||
audio.src = nextSource;
|
||||
}
|
||||
|
||||
audio.currentTime = 0;
|
||||
// Đổi nguồn rồi load() để play() không bị huỷ giữa chừng khi chuyển bài
|
||||
// trong hàng đợi — đảm bảo cảnh 1-3 phát "lời chào" xong NỐI TIẾP "Tam quan".
|
||||
audio.src = nextSource;
|
||||
audio.playbackRate = NARRATION_PLAYBACK_RATE;
|
||||
audio.volume = NARRATION_TARGET_VOLUME;
|
||||
audio.load();
|
||||
activeNarrationSrcRef.current = nextSource;
|
||||
|
||||
// Mỗi bài gắn một listener "ended" riêng (once) → khi bài này hết thì
|
||||
// tự phát bài kế tiếp trong hàng đợi. Không phụ thuộc audio.onended (dễ bị null).
|
||||
const handleEnded = () => {
|
||||
narrationEndedRef.current = null;
|
||||
void playNext();
|
||||
};
|
||||
narrationEndedRef.current = handleEnded;
|
||||
audio.addEventListener("ended", handleEnded, { once: true });
|
||||
|
||||
try {
|
||||
await audio.play();
|
||||
if (token !== narrationTokenRef.current) {
|
||||
return;
|
||||
}
|
||||
onNarrationStateChange?.(true);
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException && error.name === "AbortError") {
|
||||
@@ -1108,10 +1131,6 @@ function TourExperience({
|
||||
}
|
||||
};
|
||||
|
||||
audio.onended = () => {
|
||||
void playNext();
|
||||
};
|
||||
|
||||
void playNext();
|
||||
},
|
||||
[onNarrationStateChange],
|
||||
@@ -1205,6 +1224,10 @@ function TourExperience({
|
||||
window.clearTimeout(closingInfoTimerRef.current);
|
||||
}
|
||||
if (narrationAudio) {
|
||||
if (narrationEndedRef.current) {
|
||||
narrationAudio.removeEventListener("ended", narrationEndedRef.current);
|
||||
narrationEndedRef.current = null;
|
||||
}
|
||||
narrationAudio.onended = null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -102,7 +102,25 @@ const seeds: SceneSeed[] = [
|
||||
],
|
||||
},
|
||||
// 7 — ảnh 08: sân lớn lát đá trước gác chuông tam quan đồ sộ
|
||||
{ title: "Tiền sảnh", location: "Sân chùa & Tam quan", imageFile: 8 },
|
||||
{
|
||||
title: "Tiền sảnh",
|
||||
location: "Sân chùa & Tam quan",
|
||||
imageFile: 8,
|
||||
infoMarkers: [
|
||||
{
|
||||
id: "tien-sanh",
|
||||
title: "Tiền sảnh",
|
||||
eyebrow: EYEBROW,
|
||||
yaw: 90,
|
||||
pitch: 0,
|
||||
iconType: "info",
|
||||
content: [
|
||||
"Khu vực sân trong là một khoảng sân rộng lớn, thông thoáng, được lát phối hợp giữa gạch xám và các dải gạch đỏ tạo thành những đường vòng cung định hướng không gian. Khoảng sân được bao bọc bởi dãy nhà cổ lợp mái ngói mũi hài rêu phong, tường sơn vàng và hệ thống cửa gỗ lá bàng khép kín. Tại vị trí chính diện tòa nhà trung tâm, một đỉnh hương đá lớn được tôn trí giữa hai cột đèn đá nhỏ.",
|
||||
"Bao quanh sân là hệ thống cây cảnh và các cây duối cổ thụ được cắt tỉa, uốn thế dạng tầng tán mâm xôi bề thế, đặt trong các bồn đá tròn. Phía góc sân hướng ra bên ngoài là khoảng không gian trống có treo cờ Phật giáo, kết hợp với các dãy chậu cây cảnh xếp dọc tường bao. Phía đối diện là một lễ đài lớn ngoài trời có dựng bức bạt tạo hình đóa hoa sen tôn trí hình ảnh Đức Phật, hoàn thiện kết cấu cảnh quan xung quanh sân chính.",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// 8 — ảnh 09: sân gạch, tam quan bên trái, đài Quan Âm bên phải
|
||||
{ title: "Sân chùa", location: "Sân chùa & Tam quan", imageFile: 9 },
|
||||
// 9 — ảnh 10: đài Quan Âm lộ thiên, sân lát gạch đỏ
|
||||
@@ -126,7 +144,25 @@ const seeds: SceneSeed[] = [
|
||||
],
|
||||
},
|
||||
// 10 — ảnh 11: vườn tháp, tháp mộ gạch cổ
|
||||
{ title: "Vườn tháp cổ", location: "Sân chùa & Tam quan", imageFile: 11 },
|
||||
{
|
||||
title: "Vườn tháp cổ",
|
||||
location: "Sân chùa & Tam quan",
|
||||
imageFile: 11,
|
||||
infoMarkers: [
|
||||
{
|
||||
id: "vuon-thap-co",
|
||||
title: "Vườn tháp cổ",
|
||||
eyebrow: EYEBROW,
|
||||
yaw: 115,
|
||||
pitch: 0,
|
||||
iconType: "info",
|
||||
content: [
|
||||
"Khu vực Vườn tháp cổ được xây dựng trên một đài nền xi măng cao hơn mặt sân, có hệ thống bậc tam cấp thấp trải dài làm lối dẫn lên. Trung tâm khuôn viên là một lối đi lát đá xám phẳng phiu, hai bên bố trí đối xứng các bảo tháp ba tầng được xây bằng gạch trần màu đỏ cam kiên cố.",
|
||||
"Mỗi bảo tháp có phần chân đế đắp nổi phù điêu đá xám, thân tháp trổ các vòm cửa nhỏ hướng ra ngoài và phần mái tầng trên cùng được tạo tác cong vút với các họa tiết mây đao bằng đá xám, đỉnh tháp nhọn hướng lên nền trời. Bao bọc xung quanh các ngôi bảo tháp là hệ thống cây xanh cổ thụ tán rộng và các chậu cây cảnh tầm trung đặt trong chum gốm nung màu nâu mộc mạc.",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// 11 — ảnh 12: lối đi hẹp giữa các dãy nhà, chậu cảnh
|
||||
{ title: "Tả đạo", location: "Khu nhà Tổ & nhà khách", imageFile: 12 },
|
||||
// 12 — ảnh 14: hiên nhà có hàng cột, giàn cây xanh che mát (Khách xá)
|
||||
@@ -570,14 +606,19 @@ const mapRoutes = (() => {
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// LỒNG TIẾNG (thuyết minh) — gán file âm thanh cho từng nhóm cảnh.
|
||||
// Mỗi nhóm dùng CHUNG một file; khi khách đi giữa các cảnh trong cùng nhóm thì
|
||||
// Mỗi nhóm dùng CHUNG (các) file; khi khách đi giữa các cảnh trong cùng nhóm thì
|
||||
// audio phát LIỀN MẠCH (không tua lại) nhờ `continuousNarrationGroups` bên dưới.
|
||||
// Cảnh không có trong bảng (7, 8, 9, 10, 11) sẽ không phát thuyết minh.
|
||||
// `audio` có thể là một tên hoặc MẢNG tên — mảng sẽ phát NỐI TIẾP nhau (hàng đợi),
|
||||
// vd cảnh 1-3 phát "lời chào" xong tự phát tiếp "Tam quan".
|
||||
// Cảnh không có trong bảng (cảnh 11) sẽ không phát thuyết minh.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
const narrationGroups: { audio: string; scenes: number[] }[] = [
|
||||
{ audio: "tam-quan", scenes: [1, 2, 3] },
|
||||
const narrationGroups: { audio: string | string[]; scenes: number[] }[] = [
|
||||
{ audio: ["loi-chao", "tam-quan"], scenes: [1, 2, 3] },
|
||||
{ audio: "san-tinh-an", scenes: [4, 5] },
|
||||
{ audio: "ho-di-lac", scenes: [6] },
|
||||
{ audio: "tien-sanh", scenes: [7, 8] },
|
||||
{ audio: "ton-dai-duc-phat", scenes: [9] },
|
||||
{ audio: "vuon-thap-co", scenes: [10] },
|
||||
{ audio: "khach-xa", scenes: [12, 13, 14] },
|
||||
{ audio: "khach-duong", scenes: [15] },
|
||||
{ audio: "huu-vu-ta-vu-la-han", scenes: [16, 18] },
|
||||
@@ -587,9 +628,10 @@ const narrationGroups: { audio: string; scenes: number[] }[] = [
|
||||
|
||||
const sceneNarration = new Map<string, string[]>();
|
||||
for (const group of narrationGroups) {
|
||||
const src = audioForName(group.audio);
|
||||
const names = Array.isArray(group.audio) ? group.audio : [group.audio];
|
||||
const sources = names.map(audioForName);
|
||||
for (const scene of group.scenes) {
|
||||
sceneNarration.set(sceneId(scene), [src]);
|
||||
sceneNarration.set(sceneId(scene), sources);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,6 +640,7 @@ for (const group of narrationGroups) {
|
||||
const continuousNarrationGroups: string[][] = [
|
||||
["scene-1", "scene-2", "scene-3"],
|
||||
["scene-4", "scene-5"],
|
||||
["scene-7", "scene-8"],
|
||||
["scene-12", "scene-13", "scene-14"],
|
||||
["scene-19", "scene-20", "scene-21"],
|
||||
];
|
||||
@@ -620,7 +663,7 @@ export const chuaThaiLacTour: Omit<TourConfig, "sceneById" | "locationGroups"> =
|
||||
sceneId: "scene-9",
|
||||
},
|
||||
],
|
||||
backgroundAudio: "", // chưa có nhạc nền; engine bỏ qua chuỗi rỗng (không render thẻ audio)
|
||||
backgroundAudio: "/media/nhac-nen/nhac-phat-giao.mp3", // nhạc nền nhẹ, tự hạ thấp khi có thuyết minh
|
||||
practicalInfo: {
|
||||
intro: "Thông tin phục vụ tham quan và lễ chùa tại Chùa Thái Lạc (Pháp Vân tự).",
|
||||
items: [
|
||||
|
||||
Reference in New Issue
Block a user