Skip to content

Commit cec2501

Browse files
committed
Add livestream functionality and update environment variables
1 parent a0e8d94 commit cec2501

File tree

6 files changed

+323
-3
lines changed

6 files changed

+323
-3
lines changed

.env

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
REACT_APP_API_URL=http://192.168.120.131:32020
1+
REACT_APP_API_URL=http://192.168.120.131:32020
2+
3+
REACT_APP_API_CHAT=

src/App.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import ChangeProfile from './controller/ChangeProfile';
1414
import History from './pages/History';
1515
import Search from './pages/Search';
1616
import Livestream from './pages/Livestream';
17+
import WatchLiveStream from './pages/WatchLiveStream';
1718
function App() {
1819
useEffect(() => {
1920
document.title = "Video sharing";
@@ -34,6 +35,7 @@ function App() {
3435
<Route path="/history" element={<History />} />
3536
<Route path="/search" element={<Search />} />
3637
<Route path="/livestream" element={<Livestream />} />
38+
<Route path="/watchlivestream" element={<WatchLiveStream />}/>
3739
</Routes>
3840
</Router>
3941
);

src/assets/images/livestream.png

181 KB
Loading

src/pages/Home.js

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { IoMdMenu } from "react-icons/io";
44
import { LuMoveRight } from "react-icons/lu";
55
import VideoComponent from '../components/VideoCom/VideoComponent';
66
import Loading from '../components/Loading';
7-
7+
import LivestreamImage from '../assets/images/livestream.png'
8+
import { Link } from 'react-router-dom';
9+
import { useNavigate } from 'react-router-dom';
810
const Home = () => {
911
const [videoIds, setVideoIds] = useState([]);
1012
const [values, setValueIds] = useState();
@@ -15,6 +17,12 @@ const Home = () => {
1517
const [videoHeight, setVideoHeight] = useState('auto');
1618

1719
const [urlVideo, setUrlVideo] = useState()
20+
const navigate = useNavigate();
21+
22+
const handleLivestreamClick = (streamName) => {
23+
// Mở đường dẫn với tên stream hoặc ID stream, ví dụ `/watchlivestream/AGE1J`
24+
navigate(`/watchlivestream/${streamName}`);
25+
};
1826

1927
// useEffect(() => {
2028
// const fetchVideoIds = async () => {
@@ -142,6 +150,77 @@ const Home = () => {
142150
setIndexs( getSortedIndexes(values))
143151
},[values])
144152
const start = 4
153+
154+
const [liveStreams, setLiveStreams] = useState([]);
155+
const [streamNames, setStreamNames] = useState([]);
156+
useEffect(() => {
157+
const fetchLiveStreams = async () => {
158+
try {
159+
const response = await fetch('http://192.168.120.213:13000/stat', {
160+
headers: {
161+
Accept: 'application/xml', // Yêu cầu server trả về XML
162+
},
163+
});
164+
165+
if (!response.ok) {
166+
throw new Error('Failed to fetch live streams');
167+
}
168+
169+
const xmlText = await response.text();
170+
const parser = new DOMParser();
171+
const xmlDoc = parser.parseFromString(xmlText, 'application/xml');
172+
173+
// Tìm tất cả các <name> trong <stream>
174+
const streamElements = xmlDoc.getElementsByTagName('stream');
175+
const names = [];
176+
for (let i = 0; i < streamElements.length; i++) {
177+
const nameElement = streamElements[i].getElementsByTagName('name')[0];
178+
if (nameElement) {
179+
names.push(nameElement.textContent);
180+
}
181+
}
182+
183+
console.log('Stream names:', names);
184+
setStreamNames(names);
185+
} catch (error) {
186+
console.error('Error:', error);
187+
}
188+
};
189+
190+
fetchLiveStreams();
191+
}, []);
192+
const xmlToJson = (xml) => {
193+
const obj = {};
194+
if (xml.nodeType === 1) { // Element
195+
if (xml.attributes.length > 0) {
196+
obj['@attributes'] = {};
197+
for (let j = 0; j < xml.attributes.length; j++) {
198+
const attribute = xml.attributes.item(j);
199+
obj['@attributes'][attribute.nodeName] = attribute.nodeValue;
200+
}
201+
}
202+
} else if (xml.nodeType === 3) { // Text
203+
obj['#text'] = xml.nodeValue.trim();
204+
}
205+
206+
// Process child nodes
207+
if (xml.hasChildNodes()) {
208+
for (let i = 0; i < xml.childNodes.length; i++) {
209+
const item = xml.childNodes.item(i);
210+
const nodeName = item.nodeName;
211+
if (typeof obj[nodeName] === 'undefined') {
212+
obj[nodeName] = xmlToJson(item);
213+
} else {
214+
if (!Array.isArray(obj[nodeName])) {
215+
obj[nodeName] = [obj[nodeName]];
216+
}
217+
obj[nodeName].push(xmlToJson(item));
218+
}
219+
}
220+
}
221+
return obj;
222+
};
223+
145224
return (
146225
<div className=''>
147226
<div>
@@ -197,6 +276,40 @@ const Home = () => {
197276
<Loading/>
198277
)}
199278
</div>
279+
280+
<div className='flex items-center mb-3 pl-[20px] pt-[20px] w-3/5 text-black font-bold'>
281+
<IoMdMenu className='cursor-pointer size-[25px]' />
282+
<p className='ml-[10px] text-[#474747] text-[20px]'>Phat truc tiep</p>
283+
</div>
284+
285+
{/* Livestream section */}
286+
<div className='flex flex-wrap ml-2'>
287+
{streamNames.length > 0 ? (
288+
streamNames.map((stream, index) => (
289+
<Link
290+
// to={`/watchlivestream/${stream.name}`} // Thêm Link cho chuyển hướng
291+
to={`/watchlivestream`} // Thêm Link cho chuyển hướng
292+
293+
key={index}
294+
className='flex flex-col items-center justify-center border border-gray-300 rounded-lg p-4 m-2 bg-white shadow-md w-[300px] cursor-pointer'
295+
>
296+
{/* Hình ảnh cho mỗi livestream */}
297+
<img
298+
src={LivestreamImage}
299+
alt='Live Stream'
300+
className='w-16 h-16 object-cover mb-4'
301+
/>
302+
<p className='font-bold text-lg text-[#474747]'>{stream.name}</p>
303+
<p className='text-sm text-gray-500'>Clients: {stream.clients || 0}</p>
304+
<p className='text-sm text-gray-500'>Bandwidth In: {stream.bw_in || 0} kbps</p>
305+
<p className='text-sm text-gray-500'>Bandwidth Out: {stream.bw_out || 0} kbps</p>
306+
</Link>
307+
))
308+
) : (
309+
<Loading />
310+
)}
311+
</div>
312+
200313
</div>
201314

202315
</div>

src/pages/Livestream.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useState, useEffect } from "react";
22
import ReactPlayer from "react-player";
3-
3+
import NavbarApp from "../components/NavbarApp";
4+
import { ToastContainer } from "react-toastify";
45
// Hàm tạo mã sự kiện ngẫu nhiên gồm 5 ký tự
56
const generateEventCode = () => {
67
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
@@ -46,7 +47,13 @@ const Livestream = () => {
4647
console.log(streamUrl)
4748

4849
return (
50+
<div className=" bg-[#f0f4f9] h-screen ">
51+
<ToastContainer position='bottom-right'/>
52+
<NavbarApp/>
53+
<div className='h-[60px]'></div>
54+
4955
<div className="flex bg-[#f0f4f9] min-h-screen p-5">
56+
5057
<div className="w-2/3 p-4 bg-white rounded-lg shadow-lg">
5158
<h1 className="text-2xl font-bold mb-4">Cài đặt sự kiện phát trực tiếp</h1>
5259

@@ -72,6 +79,18 @@ const Livestream = () => {
7279

7380
</div>
7481
</div>
82+
{/* Title */}
83+
<div className="mb-6">
84+
<label className="block text-gray-700 font-semibold mb-2">Tiêu đề sự kiện:</label>
85+
<input
86+
type="text"
87+
// value={eventTitle}
88+
// onChange={(e) => setEventTitle(e.target.value)}
89+
placeholder="Nhập tiêu đề cho sự kiện..."
90+
className="border border-gray-300 rounded-lg p-2 w-full"
91+
/>
92+
</div>
93+
7594

7695
{/* Khung phát trực tiếp */}
7796
<div className="aspect-w-16 aspect-h-9 bg-black mb-4 rounded-lg">
@@ -137,6 +156,8 @@ const Livestream = () => {
137156
</div>
138157
</div>
139158
</div>
159+
</div>
160+
140161
);
141162
};
142163

src/pages/WatchLiveStream.js

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import React, { useState, useEffect } from "react";
2+
import NavbarApp from "../components/NavbarApp";
3+
import { AiOutlineLike, AiOutlineDislike } from "react-icons/ai";
4+
import { CiShare1 } from "react-icons/ci";
5+
import CopyButton from "../components/ButtonCustom/CopyButton";
6+
import ReactPlayer from "react-player";
7+
import { ToastContainer, toast } from "react-toastify";
8+
9+
const WatchLiveStream = () => {
10+
const [isStreamLive, setIsStreamLive] = useState(false); // Trạng thái livestream
11+
const [chatMessages, setChatMessages] = useState([]); // Phải là một mảng
12+
const [message, setMessage] = useState(""); // Tin nhắn hiện tại
13+
const streamUrl = `http://192.168.120.213:13000/hls/2DEKS.m3u8`;
14+
const eventCode = "event123"; // Mã sự kiện mặc định
15+
16+
// Kiểm tra trạng thái livestream
17+
const checkStreamStatus = async () => {
18+
try {
19+
const response = await fetch(streamUrl, { method: "HEAD" });
20+
setIsStreamLive(response.ok); // Kiểm tra nếu stream có sẵn
21+
} catch (error) {
22+
console.error("Lỗi khi kiểm tra trạng thái livestream:", error);
23+
setIsStreamLive(false);
24+
}
25+
};
26+
27+
// Lấy danh sách tin nhắn từ API
28+
const fetchChatMessages = async () => {
29+
try {
30+
const response = await fetch(`${process.env.REACT_APP_API_CHAT}/chat/event123`);
31+
32+
// Kiểm tra nếu không phải là JSON hợp lệ, có thể là HTML do lỗi
33+
const text = await response.text(); // Đọc phản hồi dưới dạng văn bản
34+
if (response.ok) {
35+
try {
36+
const data = JSON.parse(text); // Thử chuyển thành JSON
37+
if (Array.isArray(data.messages)) {
38+
setChatMessages(data.messages);
39+
} else {
40+
console.error("Dữ liệu messages không phải là mảng:", data.messages);
41+
setChatMessages([]);
42+
}
43+
} catch (error) {
44+
console.error("Lỗi khi phân tích JSON:", error);
45+
console.error("Dữ liệu trả về:", text); // In ra nội dung trả về để kiểm tra
46+
}
47+
} else {
48+
console.error("Lỗi khi gọi API:", response.status);
49+
console.error("Nội dung trả về:", text); // In ra nội dung trả về để kiểm tra
50+
setChatMessages([]);
51+
}
52+
} catch (error) {
53+
console.error("Lỗi khi gọi API:", error);
54+
setChatMessages([]);
55+
}
56+
};
57+
58+
59+
60+
61+
useEffect(() => {
62+
checkStreamStatus(); // Kiểm tra trạng thái khi tải trang
63+
fetchChatMessages(); // Lấy tin nhắn khi tải trang
64+
}, [streamUrl]);
65+
const username = localStorage.getItem("userName");
66+
// Xử lý gửi tin nhắn
67+
const handleSendMessage = async () => {
68+
if (message.trim()) {
69+
const newMessage = { username: username, message, eventCode }; // Tin nhắn mới
70+
try {
71+
const response = await fetch(`${process.env.REACT_APP_API_CHAT}/chat`, {
72+
method: "POST",
73+
headers: { "Content-Type": "application/json" },
74+
body: JSON.stringify(newMessage),
75+
});
76+
if (response.ok) {
77+
toast.success("Gửi tin nhắn thành công!");
78+
setMessage(""); // Xóa ô nhập sau khi gửi
79+
fetchChatMessages(); // Tải lại danh sách tin nhắn
80+
} else {
81+
toast.error("Lỗi khi gửi tin nhắn!");
82+
console.error("Lỗi khi gửi tin nhắn:", response.status);
83+
}
84+
} catch (error) {
85+
toast.error("Không thể gửi tin nhắn!");
86+
console.error("Lỗi khi gọi API gửi tin nhắn:", error);
87+
}
88+
}
89+
};
90+
91+
return (
92+
<div className="bg-[#f0f4f9] h-screen">
93+
<ToastContainer position="bottom-right" />
94+
<NavbarApp />
95+
<div className="h-[60px]"></div>
96+
<div className="container mx-auto px-4 py-6">
97+
{/* Sắp xếp hàng ngang */}
98+
<div className="flex flex-col md:flex-row gap-6">
99+
{/* Video Player và chi tiết */}
100+
<div className="flex-1 bg-white shadow-md rounded-lg p-4">
101+
<div className="aspect-w-16 aspect-h-9 bg-black rounded-lg">
102+
{isStreamLive ? (
103+
<ReactPlayer
104+
url={streamUrl}
105+
playing
106+
controls
107+
width="100%"
108+
height="100%"
109+
config={{ file: { forceHLS: true } }}
110+
/>
111+
) : (
112+
<p className="text-center text-gray-500 py-10">
113+
Chưa có livestream nào đang phát.
114+
</p>
115+
)}
116+
</div>
117+
118+
{/* Thông tin livestream */}
119+
<div className="mt-4 flex justify-between items-center">
120+
<div className="flex space-x-4">
121+
<button className="flex items-center text-gray-700 hover:text-blue-600">
122+
<AiOutlineLike className="mr-1" size={20} />
123+
<span>Like</span>
124+
</button>
125+
<button className="flex items-center text-gray-700 hover:text-red-600">
126+
<AiOutlineDislike className="mr-1" size={20} />
127+
<span>Dislike</span>
128+
</button>
129+
<button className="flex items-center text-gray-700 hover:text-green-600">
130+
<CiShare1 className="mr-1" size={20} />
131+
<span>Share</span>
132+
</button>
133+
</div>
134+
<CopyButton textToCopy={streamUrl} />
135+
</div>
136+
</div>
137+
138+
{/* Chat box */}
139+
<div className="w-full md:w-1/3 bg-white rounded-lg shadow-lg flex flex-col">
140+
<h2 className="text-xl font-bold mb-4 p-4">Trò chuyện trực tiếp</h2>
141+
142+
{/* Hiển thị tin nhắn trò chuyện */}
143+
<div className="flex-grow overflow-y-auto mb-4 border rounded-lg p-4 bg-gray-50">
144+
{Array.isArray(chatMessages) && chatMessages.length === 0 ? (
145+
<p className="text-gray-500 text-center">Chưa có tin nhắn nào.</p>
146+
) : (
147+
Array.isArray(chatMessages) && chatMessages.map((msg, index) => (
148+
<div key={index} className="mb-2">
149+
<p className="text-sm">
150+
<span className="font-semibold">{msg.username}:</span> {msg.message}
151+
</p>
152+
<p className="text-xs text-gray-400">{msg.timestamp}</p>
153+
</div>
154+
))
155+
)}
156+
</div>
157+
158+
159+
{/* Nhập và gửi tin nhắn */}
160+
<div className="flex items-center p-4">
161+
<input
162+
type="text"
163+
value={message}
164+
onChange={(e) => setMessage(e.target.value)}
165+
placeholder="Nhập tin nhắn..."
166+
className="border rounded-lg p-2 flex-grow mr-2"
167+
/>
168+
<button
169+
onClick={handleSendMessage}
170+
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
171+
>
172+
Gửi
173+
</button>
174+
</div>
175+
</div>
176+
</div>
177+
</div>
178+
</div>
179+
);
180+
};
181+
182+
export default WatchLiveStream;

0 commit comments

Comments
 (0)