mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 09:35:42 +00:00
Merge pull request #540 from chrisk325/subtitle-info
Feat: add audio track info and subtitle track info to exoplayer
This commit is contained in:
commit
18f49f0853
4 changed files with 200 additions and 17 deletions
|
|
@ -1599,6 +1599,29 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
Track audioTrack = exoplayerTrackToGenericTrack(format, groupIndex, selection, group);
|
||||
audioTrack.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate);
|
||||
audioTrack.setSelected(isSelected);
|
||||
// Encode channel count, bitrate and mimeType into title so JS can read them reliably
|
||||
// e.g. "English|ch:6|br:640000|mt:audio/ac3"
|
||||
String existing = audioTrack.getTitle() != null ? audioTrack.getTitle() : "";
|
||||
if (format.channelCount != Format.NO_VALUE && format.channelCount > 0) {
|
||||
existing = existing + "|ch:" + format.channelCount;
|
||||
}
|
||||
// Use bitrate, fall back to averageBitrate then peakBitrate
|
||||
int effectiveBitrate = format.bitrate;
|
||||
if (effectiveBitrate == Format.NO_VALUE || effectiveBitrate <= 0) {
|
||||
effectiveBitrate = format.averageBitrate;
|
||||
}
|
||||
if (effectiveBitrate == Format.NO_VALUE || effectiveBitrate <= 0) {
|
||||
effectiveBitrate = format.peakBitrate;
|
||||
}
|
||||
if (effectiveBitrate != Format.NO_VALUE && effectiveBitrate > 0) {
|
||||
existing = existing + "|br:" + effectiveBitrate;
|
||||
}
|
||||
if (format.sampleMimeType != null && !format.sampleMimeType.isEmpty()) {
|
||||
existing = existing + "|mt:" + format.sampleMimeType;
|
||||
}
|
||||
if (!existing.isEmpty()) {
|
||||
audioTrack.setTitle(existing);
|
||||
}
|
||||
audioTracks.add(audioTrack);
|
||||
}
|
||||
|
||||
|
|
@ -1794,7 +1817,25 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||
Track track = new Track();
|
||||
track.setIndex(groupIndex);
|
||||
track.setLanguage(format.language != null ? format.language : "unknown");
|
||||
track.setTitle(format.label != null ? format.label : "Track " + (groupIndex + 1));
|
||||
String baseTitle = format.label != null ? format.label : "";
|
||||
if (format.channelCount != Format.NO_VALUE && format.channelCount > 0) {
|
||||
baseTitle = baseTitle + "|ch:" + format.channelCount;
|
||||
}
|
||||
// Use bitrate, fall back to averageBitrate then peakBitrate
|
||||
int effectiveBitrate = format.bitrate;
|
||||
if (effectiveBitrate == Format.NO_VALUE || effectiveBitrate <= 0) {
|
||||
effectiveBitrate = format.averageBitrate;
|
||||
}
|
||||
if (effectiveBitrate == Format.NO_VALUE || effectiveBitrate <= 0) {
|
||||
effectiveBitrate = format.peakBitrate;
|
||||
}
|
||||
if (effectiveBitrate != Format.NO_VALUE && effectiveBitrate > 0) {
|
||||
baseTitle = baseTitle + "|br:" + effectiveBitrate;
|
||||
}
|
||||
if (format.sampleMimeType != null && !format.sampleMimeType.isEmpty()) {
|
||||
baseTitle = baseTitle + "|mt:" + format.sampleMimeType;
|
||||
}
|
||||
track.setTitle(baseTitle);
|
||||
track.setSelected(false); // Don't report selection status - let PlayerView handle it
|
||||
if (format.sampleMimeType != null)
|
||||
track.setMimeType(format.sampleMimeType);
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ import { storageService } from '../../services/storageService';
|
|||
import stremioService from '../../services/stremioService';
|
||||
import { WyzieSubtitle, SubtitleCue } from './utils/playerTypes';
|
||||
import { findBestSubtitleTrack, findBestAudioTrack } from './utils/trackSelectionUtils';
|
||||
import { buildExoAudioTrackName, buildExoSubtitleTrackName } from './android/components/VideoSurface';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import axios from 'axios';
|
||||
|
||||
|
|
@ -347,20 +348,22 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
}
|
||||
|
||||
if (data.audioTracks) {
|
||||
console.log('[TrackDebug] raw audioTracks:', JSON.stringify(data.audioTracks));
|
||||
const formatted = data.audioTracks.map((t: any, i: number) => ({
|
||||
// react-native-video selectedAudioTrack {type:'index'} uses 0-based list index.
|
||||
id: i,
|
||||
name: t.title || t.name || `Track ${i + 1}`,
|
||||
name: buildExoAudioTrackName(t, i),
|
||||
language: t.language
|
||||
}));
|
||||
tracksHook.setRnVideoAudioTracks(formatted);
|
||||
}
|
||||
if (data.textTracks) {
|
||||
console.log('[TrackDebug] raw textTracks:', JSON.stringify(data.textTracks));
|
||||
const formatted = data.textTracks.map((t: any, i: number) => ({
|
||||
// react-native-video selectedTextTrack {type:'index'} uses 0-based list index.
|
||||
// Using `t.index` can be non-unique/misaligned and breaks selection/rendering.
|
||||
id: i,
|
||||
name: t.title || t.name || `Track ${i + 1}`,
|
||||
name: buildExoSubtitleTrackName(t, i),
|
||||
language: t.language
|
||||
}));
|
||||
tracksHook.setRnVideoTextTracks(formatted);
|
||||
|
|
|
|||
|
|
@ -78,6 +78,141 @@ const isCodecError = (errorString: string): boolean => {
|
|||
});
|
||||
};
|
||||
|
||||
const EXOPLAYER_LANG_MAP: Record<string, string> = {
|
||||
en: 'English', eng: 'English',
|
||||
es: 'Spanish', spa: 'Spanish',
|
||||
fr: 'French', fre: 'French',
|
||||
de: 'German', ger: 'German',
|
||||
it: 'Italian', ita: 'Italian',
|
||||
ja: 'Japanese', jpn: 'Japanese',
|
||||
ko: 'Korean', kor: 'Korean',
|
||||
zh: 'Chinese', chi: 'Chinese',
|
||||
ru: 'Russian', rus: 'Russian',
|
||||
pt: 'Portuguese', por: 'Portuguese',
|
||||
hi: 'Hindi', hin: 'Hindi',
|
||||
ar: 'Arabic', ara: 'Arabic',
|
||||
nl: 'Dutch', dut: 'Dutch',
|
||||
pl: 'Polish', pol: 'Polish',
|
||||
tr: 'Turkish', tur: 'Turkish',
|
||||
};
|
||||
|
||||
const exoMimeToCodec = (mimeType?: string): string | null => {
|
||||
if (!mimeType) return null;
|
||||
const mime = mimeType.toLowerCase();
|
||||
if (mime.includes('eac3') || mime.includes('ec-3')) return 'Dolby Digital Plus';
|
||||
if (mime.includes('ac3') || mime.includes('ac-3')) return 'Dolby Digital';
|
||||
if (mime.includes('truehd')) return 'TrueHD';
|
||||
if (mime.includes('dts.hd') || mime.includes('dts-hd') || mime.includes('dtshd')) return 'DTS-HD';
|
||||
if (mime.includes('dts.uhd') || mime.includes('dts:x')) return 'DTS:X';
|
||||
if (mime.includes('dts')) return 'DTS';
|
||||
if (mime.includes('aac')) return 'AAC';
|
||||
if (mime.includes('opus')) return 'Opus';
|
||||
if (mime.includes('vorbis')) return 'Vorbis';
|
||||
if (mime.includes('mp4a') || mime.includes('mpeg')) return 'MP3';
|
||||
if (mime.includes('flac')) return 'FLAC';
|
||||
return null;
|
||||
};
|
||||
|
||||
export const buildExoAudioTrackName = (t: any, i: number): string => {
|
||||
const parts: string[] = [];
|
||||
|
||||
// Check both title and name fields for encoded metadata from Java
|
||||
let rawTitle: string = t.title ?? t.name ?? '';
|
||||
let channelCount: number | null = null;
|
||||
let encodedBitrate: number | null = null;
|
||||
let encodedMimeType: string | null = null;
|
||||
|
||||
// Extract |ch:N, |br:N, |mt:mime from title
|
||||
const chMatch = rawTitle.match(/\|ch:(\d+)/);
|
||||
if (chMatch) channelCount = parseInt(chMatch[1], 10);
|
||||
|
||||
const brMatch = rawTitle.match(/\|br:(\d+)/);
|
||||
if (brMatch) encodedBitrate = parseInt(brMatch[1], 10);
|
||||
|
||||
const mtMatch = rawTitle.match(/\|mt:([^|]+)/);
|
||||
if (mtMatch) encodedMimeType = mtMatch[1];
|
||||
|
||||
// Strip all encoded metadata from the display title
|
||||
rawTitle = rawTitle.replace(/\|ch:\d+/g, '').replace(/\|br:\d+/g, '').replace(/\|mt:[^|]+/g, '').trim();
|
||||
|
||||
if (rawTitle) {
|
||||
if (t.language) {
|
||||
const lang = EXOPLAYER_LANG_MAP[t.language.toLowerCase()] ?? t.language.toUpperCase();
|
||||
if (!rawTitle.toLowerCase().includes(lang.toLowerCase())) {
|
||||
parts.push(lang);
|
||||
}
|
||||
}
|
||||
parts.push(rawTitle);
|
||||
} else if (t.language) {
|
||||
parts.push(EXOPLAYER_LANG_MAP[t.language.toLowerCase()] ?? t.language.toUpperCase());
|
||||
}
|
||||
|
||||
// Use mimeType from track object, fall back to encoded mimeType from title
|
||||
const mimeType = t.mimeType ?? encodedMimeType ?? null;
|
||||
const codec = exoMimeToCodec(mimeType);
|
||||
// Only append codec if title doesn't already mention it
|
||||
const titleLowerForCodec = rawTitle.toLowerCase();
|
||||
const codecAlreadyInTitle = titleLowerForCodec.includes('dolby') ||
|
||||
titleLowerForCodec.includes('dts') ||
|
||||
titleLowerForCodec.includes('atmos') ||
|
||||
titleLowerForCodec.includes('aac') ||
|
||||
titleLowerForCodec.includes('truehd') ||
|
||||
titleLowerForCodec.includes('flac');
|
||||
if (codec && !codecAlreadyInTitle) parts.push(codec);
|
||||
|
||||
// Use parsed channel count, fall back to bitrate-based guess for AC3/EAC3
|
||||
let ch = channelCount ?? t.channelCount ?? null;
|
||||
if (ch == null) {
|
||||
const mime = (mimeType ?? '').toLowerCase();
|
||||
const br = encodedBitrate ?? t.bitrate ?? 0;
|
||||
if (mime.includes('ac3') || mime.includes('eac3') || mime.includes('ec-3')) {
|
||||
if (br >= 500000) ch = 6;
|
||||
else if (br > 0 && br <= 320000) ch = 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (ch != null && ch > 0) {
|
||||
if (ch === 8) parts.push('7.1');
|
||||
else if (ch === 7) parts.push('6.1');
|
||||
else if (ch === 6) parts.push('5.1');
|
||||
else if (ch === 2) parts.push('2.0');
|
||||
else if (ch === 1) parts.push('Mono');
|
||||
else parts.push(`${ch}ch`);
|
||||
}
|
||||
|
||||
const bitrate = encodedBitrate ?? t.bitrate ?? t.bitRate ?? null;
|
||||
if (bitrate != null && bitrate > 0) {
|
||||
parts.push(`${Math.round(bitrate / 1000)} kbps`);
|
||||
}
|
||||
|
||||
return parts.length > 0 ? parts.join(' ') : `Track ${i + 1}`;
|
||||
};
|
||||
|
||||
|
||||
export const buildExoSubtitleTrackName = (t: any, i: number): string => {
|
||||
const parts: string[] = [];
|
||||
const titleLower = (t.title ?? '').toLowerCase();
|
||||
if (t.title && t.title.trim()) {
|
||||
// Prepend language if available and not already in the title
|
||||
if (t.language) {
|
||||
const lang = EXOPLAYER_LANG_MAP[t.language.toLowerCase()] ?? t.language.toUpperCase();
|
||||
if (!t.title.toLowerCase().includes(lang.toLowerCase())) {
|
||||
parts.push(lang);
|
||||
}
|
||||
}
|
||||
parts.push(t.title.trim());
|
||||
} else if (t.language) {
|
||||
parts.push(EXOPLAYER_LANG_MAP[t.language.toLowerCase()] ?? t.language.toUpperCase());
|
||||
}
|
||||
if (t.isHearingImpaired || titleLower.includes('sdh') || titleLower.includes('hearing impaired') || titleLower.includes('cc')) {
|
||||
if (!titleLower.includes('sdh')) parts.push('SDH');
|
||||
}
|
||||
if (t.isForced || titleLower.includes('forced')) {
|
||||
if (!titleLower.includes('forced')) parts.push('Forced');
|
||||
}
|
||||
return parts.length > 0 ? parts.join(' ') : `Track ${i + 1}`;
|
||||
};
|
||||
|
||||
export const VideoSurface: React.FC<VideoSurfaceProps> = ({
|
||||
processedStreamUrl,
|
||||
videoType,
|
||||
|
|
@ -224,13 +359,13 @@ export const VideoSurface: React.FC<VideoSurfaceProps> = ({
|
|||
const handleExoLoad = (data: any) => {
|
||||
const audioTracks = data.audioTracks?.map((t: any, i: number) => ({
|
||||
id: i,
|
||||
name: t.title || t.language || `Track ${i + 1}`,
|
||||
name: buildExoAudioTrackName(t, i),
|
||||
language: t.language,
|
||||
})) ?? [];
|
||||
|
||||
const subtitleTracks = data.textTracks?.map((t: any, i: number) => ({
|
||||
id: i,
|
||||
name: t.title || t.language || `Track ${i + 1}`,
|
||||
name: buildExoSubtitleTrackName(t, i),
|
||||
language: t.language,
|
||||
})) ?? [];
|
||||
|
||||
|
|
|
|||
|
|
@ -102,18 +102,20 @@ export const getTrackDisplayName = (track: { name?: string, id: number, language
|
|||
return track.name;
|
||||
}
|
||||
|
||||
// If the track name contains detailed information (like codec, bitrate, etc.), use it as-is
|
||||
// If the track name contains detailed information, use it as-is
|
||||
if (track.name && (track.name.includes('DDP') || track.name.includes('DTS') || track.name.includes('AAC') ||
|
||||
track.name.includes('Kbps') || track.name.includes('Atmos') || track.name.includes('~'))) {
|
||||
track.name.includes('EAC3') || track.name.includes('AC3') || track.name.includes('TrueHD') ||
|
||||
track.name.includes('Dolby') || track.name.includes('FLAC') || track.name.includes('Opus') ||
|
||||
track.name.includes('Kbps') || track.name.includes('kbps') || track.name.includes('Atmos') ||
|
||||
track.name.includes('5.1') || track.name.includes('7.1') || track.name.includes('6.1') || track.name.includes('2.0') ||
|
||||
track.name.includes('SDH') || track.name.includes('Forced') || track.name.includes('~'))) {
|
||||
return track.name;
|
||||
}
|
||||
|
||||
// If we have a language field, use that for better display (only for simple track names)
|
||||
if (track.language && track.language !== 'Unknown') {
|
||||
const formattedLanguage = formatLanguage(track.language);
|
||||
if (formattedLanguage !== 'Unknown' && !formattedLanguage.includes('Unknown')) {
|
||||
return formattedLanguage;
|
||||
}
|
||||
// If name is a rich multi-word label (more than one word and not a generic track name), use it as-is
|
||||
const genericTrackMatch = track.name.match(/^(Audio|Track|Subtitle)\s+(\d+)$/i);
|
||||
if (!genericTrackMatch && track.name.trim().includes(' ')) {
|
||||
return track.name;
|
||||
}
|
||||
|
||||
// Try to extract language from name like "Some Info - [English]"
|
||||
|
|
@ -122,10 +124,12 @@ export const getTrackDisplayName = (track: { name?: string, id: number, language
|
|||
return languageMatch[1];
|
||||
}
|
||||
|
||||
// Handle generic VLC track names like "Audio 1", "Track 1"
|
||||
const genericTrackMatch = track.name.match(/^(Audio|Track)\s+(\d+)$/i);
|
||||
// Handle generic VLC track names like "Audio 1", "Track 1" — use language if available
|
||||
if (genericTrackMatch) {
|
||||
return `Audio ${genericTrackMatch[2]}`;
|
||||
if (track.language && track.language !== 'Unknown') {
|
||||
return formatLanguage(track.language);
|
||||
}
|
||||
return track.name;
|
||||
}
|
||||
|
||||
// Check for common language patterns in the name
|
||||
|
|
@ -231,4 +235,4 @@ export const getHlsHeaders = () => {
|
|||
...defaultAndroidHeaders,
|
||||
'Accept': 'application/x-mpegURL, application/vnd.apple.mpegurl, application/json, text/plain',
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue