mirror of
https://github.com/zimplexing/OrionTV.git
synced 2026-02-15 20:34:43 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4e4417ef6 | ||
|
|
64cdcb78b6 | ||
|
|
809422f702 | ||
|
|
1c9b3b2553 | ||
|
|
e02b3c512f |
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## ✨ 功能特性
|
## ✨ 功能特性
|
||||||
|
|
||||||
- **跨平台支持**: 同时支持 Apple TV 和 Android TV。
|
- **框架跨平台支持**: 同时支持构建 Apple TV 和 Android TV。
|
||||||
- **现代化前端**: 使用 Expo、React Native TVOS 和 TypeScript 构建,性能卓越。
|
- **现代化前端**: 使用 Expo、React Native TVOS 和 TypeScript 构建,性能卓越。
|
||||||
- **Expo Router**: 基于文件系统的路由,使导航逻辑清晰简单。
|
- **Expo Router**: 基于文件系统的路由,使导航逻辑清晰简单。
|
||||||
- **后端服务**: 配套 Express 后端,用于处理数据获取、搜索和详情展示。
|
- **后端服务**: 配套 Express 后端,用于处理数据获取、搜索和详情展示。
|
||||||
@@ -52,10 +52,8 @@
|
|||||||
接下来,在项目根目录运行前端应用:
|
接下来,在项目根目录运行前端应用:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# (如果还在 backend 目录) 返回根目录
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
# 安装前端依赖
|
# 安装依赖
|
||||||
yarn
|
yarn
|
||||||
|
|
||||||
# [首次运行或依赖更新后] 生成原生项目文件
|
# [首次运行或依赖更新后] 生成原生项目文件
|
||||||
@@ -74,6 +72,7 @@ yarn android-tv
|
|||||||
- 1.2.x 以上版本需配合 [MoonTV](https://github.com/senshinya/MoonTV) 部署使用,api 地址填部MoonTV署后的访问地址。
|
- 1.2.x 以上版本需配合 [MoonTV](https://github.com/senshinya/MoonTV) 部署使用,api 地址填部MoonTV署后的访问地址。
|
||||||
|
|
||||||
- **注意:** 地址后面不要带 `/` ,不要遗漏 `http://` 或者 `https://`
|
- **注意:** 地址后面不要带 `/` ,不要遗漏 `http://` 或者 `https://`
|
||||||
|
- 如果部署在CF,请确保电视端可以访问,不然会出现无法登录或者登录项与自己配置不符的问题
|
||||||
|
|
||||||
- 如果不想依赖 MoonTV,可以使用 1.1.x 版本。
|
- 如果不想依赖 MoonTV,可以使用 1.1.x 版本。
|
||||||
|
|
||||||
|
|||||||
1
app.json
1
app.json
@@ -38,6 +38,7 @@
|
|||||||
"android": {
|
"android": {
|
||||||
"package": "com.oriontv",
|
"package": "com.oriontv",
|
||||||
"usesCleartextTraffic": true,
|
"usesCleartextTraffic": true,
|
||||||
|
"hardwareAcceleration": true,
|
||||||
"networkSecurityConfig": "@xml/network_security_config",
|
"networkSecurityConfig": "@xml/network_security_config",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/icon.png",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default function LiveScreen() {
|
|||||||
<View style={styles.groupColumn}>
|
<View style={styles.groupColumn}>
|
||||||
<FlatList
|
<FlatList
|
||||||
data={channelGroups}
|
data={channelGroups}
|
||||||
keyExtractor={(item) => item}
|
keyExtractor={(item, index) => `group-${item}-${index}`}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<StyledButton
|
<StyledButton
|
||||||
text={item}
|
text={item}
|
||||||
@@ -124,7 +124,7 @@ export default function LiveScreen() {
|
|||||||
) : (
|
) : (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={groupedChannels[selectedGroup] || []}
|
data={groupedChannels[selectedGroup] || []}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item, index) => `${item.id}-${item.group}-${index}`}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<StyledButton
|
<StyledButton
|
||||||
text={item.name || "Unknown Channel"}
|
text={item.name || "Unknown Channel"}
|
||||||
@@ -190,6 +190,8 @@ const styles = StyleSheet.create({
|
|||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
paddingHorizontal: 4,
|
paddingHorizontal: 4,
|
||||||
marginVertical: 4,
|
marginVertical: 4,
|
||||||
|
paddingLeft: 10,
|
||||||
|
paddingRight: 10,
|
||||||
},
|
},
|
||||||
groupButtonText: {
|
groupButtonText: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
@@ -198,6 +200,8 @@ const styles = StyleSheet.create({
|
|||||||
paddingVertical: 6,
|
paddingVertical: 6,
|
||||||
paddingHorizontal: 4,
|
paddingHorizontal: 4,
|
||||||
marginVertical: 3,
|
marginVertical: 3,
|
||||||
|
paddingLeft: 16,
|
||||||
|
paddingRight: 16,
|
||||||
},
|
},
|
||||||
channelItemText: {
|
channelItemText: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|||||||
@@ -42,13 +42,13 @@ export default function PlayScreen() {
|
|||||||
const {
|
const {
|
||||||
isLoading,
|
isLoading,
|
||||||
showControls,
|
showControls,
|
||||||
showNextEpisodeOverlay,
|
// showNextEpisodeOverlay,
|
||||||
initialPosition,
|
initialPosition,
|
||||||
introEndTime,
|
introEndTime,
|
||||||
setVideoRef,
|
setVideoRef,
|
||||||
handlePlaybackStatusUpdate,
|
handlePlaybackStatusUpdate,
|
||||||
setShowControls,
|
setShowControls,
|
||||||
setShowNextEpisodeOverlay,
|
// setShowNextEpisodeOverlay,
|
||||||
reset,
|
reset,
|
||||||
loadVideo,
|
loadVideo,
|
||||||
} = usePlayerStore();
|
} = usePlayerStore();
|
||||||
@@ -151,7 +151,7 @@ export default function PlayScreen() {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<NextEpisodeOverlay visible={showNextEpisodeOverlay} onCancel={() => setShowNextEpisodeOverlay(false)} />
|
{/* <NextEpisodeOverlay visible={showNextEpisodeOverlay} onCancel={() => setShowNextEpisodeOverlay(false)} /> */}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<EpisodeSelectionModal />
|
<EpisodeSelectionModal />
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default function LivePlayer({ streamUrl, channelTitle, onPlaybackStatusUp
|
|||||||
if (!streamUrl) {
|
if (!streamUrl) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.messageText}>Select a channel to play.</Text>
|
<Text style={styles.messageText}>按向下键选择频道</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,7 @@ export default function LivePlayer({ streamUrl, channelTitle, onPlaybackStatusUp
|
|||||||
if (isTimeout) {
|
if (isTimeout) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.messageText}>Failed to load stream. It might be offline or unavailable.</Text>
|
<Text style={styles.messageText}>加载失败,请重试</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -98,7 +98,7 @@ export default function LivePlayer({ streamUrl, channelTitle, onPlaybackStatusUp
|
|||||||
{isLoading && (
|
{isLoading && (
|
||||||
<View style={styles.loadingOverlay}>
|
<View style={styles.loadingOverlay}>
|
||||||
<ActivityIndicator size="large" color="#fff" />
|
<ActivityIndicator size="large" color="#fff" />
|
||||||
<Text style={styles.messageText}>Loading...</Text>
|
<Text style={styles.messageText}>加载中...</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{channelTitle && !isLoading && !isTimeout && (
|
{channelTitle && !isLoading && !isTimeout && (
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "OrionTV",
|
"name": "OrionTV",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "expo-router/entry",
|
"main": "expo-router/entry",
|
||||||
"version": "1.2.5",
|
"version": "1.2.6",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start",
|
"start": "EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start",
|
||||||
"start-tv": "EXPO_TV=1 EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start",
|
"start-tv": "EXPO_TV=1 EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start",
|
||||||
|
|||||||
@@ -16,23 +16,37 @@ export const parseM3U = (m3uText: string): Channel[] => {
|
|||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmedLine = line.trim();
|
const trimmedLine = line.trim();
|
||||||
if (trimmedLine.startsWith('#EXTINF:')) {
|
if (trimmedLine.startsWith('#EXTINF:')) {
|
||||||
currentChannelInfo = { id: '', name: '', url: '', logo: '', group: '' };
|
currentChannelInfo = {}; // Start a new channel
|
||||||
const commaIndex = trimmedLine.indexOf(',');
|
const commaIndex = trimmedLine.lastIndexOf(',');
|
||||||
if (commaIndex !== -1) {
|
if (commaIndex !== -1) {
|
||||||
currentChannelInfo.name = trimmedLine.substring(commaIndex + 1).trim();
|
currentChannelInfo.name = trimmedLine.substring(commaIndex + 1).trim();
|
||||||
const attributesPart = trimmedLine.substring(8, commaIndex);
|
const attributesPart = trimmedLine.substring(8, commaIndex);
|
||||||
const logoMatch = attributesPart.match(/tvg-logo="([^"]*)"/i);
|
const logoMatch = attributesPart.match(/tvg-logo="([^"]*)"/i);
|
||||||
if (logoMatch && logoMatch[1]) currentChannelInfo.logo = logoMatch[1];
|
if (logoMatch && logoMatch[1]) {
|
||||||
|
currentChannelInfo.logo = logoMatch[1];
|
||||||
|
}
|
||||||
const groupMatch = attributesPart.match(/group-title="([^"]*)"/i);
|
const groupMatch = attributesPart.match(/group-title="([^"]*)"/i);
|
||||||
if (groupMatch && groupMatch[1]) currentChannelInfo.group = groupMatch[1];
|
if (groupMatch && groupMatch[1]) {
|
||||||
|
currentChannelInfo.group = groupMatch[1];
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
currentChannelInfo.name = trimmedLine.substring(8).trim();
|
currentChannelInfo.name = trimmedLine.substring(8).trim();
|
||||||
}
|
}
|
||||||
} else if (currentChannelInfo && trimmedLine && !trimmedLine.startsWith('#') && trimmedLine.includes('://')) {
|
} else if (currentChannelInfo && trimmedLine && !trimmedLine.startsWith('#') && trimmedLine.includes('://')) {
|
||||||
currentChannelInfo.url = trimmedLine;
|
currentChannelInfo.url = trimmedLine;
|
||||||
currentChannelInfo.id = currentChannelInfo.url; // Use URL as ID
|
currentChannelInfo.id = currentChannelInfo.url; // Use URL as ID
|
||||||
parsedChannels.push(currentChannelInfo as Channel);
|
|
||||||
currentChannelInfo = null;
|
// Ensure all required fields are present, providing defaults if necessary
|
||||||
|
const finalChannel: Channel = {
|
||||||
|
id: currentChannelInfo.id,
|
||||||
|
url: currentChannelInfo.url,
|
||||||
|
name: currentChannelInfo.name || 'Unknown',
|
||||||
|
logo: currentChannelInfo.logo || '',
|
||||||
|
group: currentChannelInfo.group || 'Default',
|
||||||
|
};
|
||||||
|
|
||||||
|
parsedChannels.push(finalChannel);
|
||||||
|
currentChannelInfo = null; // Reset for the next channel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return parsedChannels;
|
return parsedChannels;
|
||||||
@@ -57,14 +71,14 @@ export const getPlayableUrl = (originalUrl: string | null): string | null => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// In React Native, we use the proxy for all http streams to avoid potential issues.
|
// In React Native, we use the proxy for all http streams to avoid potential issues.
|
||||||
if (originalUrl.toLowerCase().startsWith('http://')) {
|
// if (originalUrl.toLowerCase().startsWith('http://')) {
|
||||||
// Use the baseURL from the existing api instance.
|
// // Use the baseURL from the existing api instance.
|
||||||
if (!api.baseURL) {
|
// if (!api.baseURL) {
|
||||||
console.warn("API base URL is not set. Cannot create proxy URL.")
|
// console.warn("API base URL is not set. Cannot create proxy URL.")
|
||||||
return originalUrl; // Fallback to original URL
|
// return originalUrl; // Fallback to original URL
|
||||||
}
|
// }
|
||||||
return `${api.baseURL}/proxy?url=${encodeURIComponent(originalUrl)}`;
|
// return `${api.baseURL}/proxy?url=${encodeURIComponent(originalUrl)}`;
|
||||||
}
|
// }
|
||||||
// HTTPS streams can be played directly.
|
// HTTPS streams can be played directly.
|
||||||
return originalUrl;
|
return originalUrl;
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user