If you’re working with video processing in React Native and need to generate subtitles, you might have encountered issues with ASS (.ass) subtitle files—especially with missing subtitles when using FFmpeg. In this article, we’ll explore how to correctly create ASS subtitle files using React Native and JavaScript, and solve the common problem of missing subtitles in the output video.
Introduction
Creating subtitles for videos is a common requirement in multimedia applications. The ASS (Advanced SubStation Alpha) format is a popular choice due to its advanced styling capabilities. However, when generating ASS files programmatically, especially in React Native, you might run into issues where certain subtitles don’t display as expected when processed with FFmpeg.
Understanding the ASS Subtitle File Format
The ASS format is a plain-text file that contains styling and timing information for subtitles. It has a specific structure divided into sections:
[Script Info]
: Metadata about the script.[V4+ Styles]
: Definitions of styles used in the subtitles.[Events]
: The actual subtitle entries.
Each section and line must be correctly formatted and separated by newline characters for FFmpeg and media players to parse them properly.
The Problem: Missing Subtitles
When generating ASS files in React Native, you might notice that certain subtitles, often the second one, are consistently missing in the output video after processing with FFmpeg. This issue can be frustrating and time-consuming to debug.
Common Causes
- Incorrect File Formatting: Missing or incorrect newline characters can cause the ASS file to be improperly formatted.
- File Encoding Issues: Using the wrong encoding or including a Byte Order Mark (BOM) can lead to parsing errors.
The Solution: Correctly Formatting the ASS File
The primary cause of missing subtitles in this context is improper formatting of the ASS file, specifically the lack of necessary newline characters. By ensuring that each line and section in the ASS file is correctly terminated and separated with \n
, you enable FFmpeg to parse and process the subtitles accurately.
Step-by-Step Guide to Creating ASS Subtitles in React Native
1. Define the Subtitle Generation Function
Create a function that generates the ASS subtitle content based on your subtitle data. This function will build the necessary sections and entries.
Important Detail
Be sure to include the \n after each line of the .ass string.
Without this \n escape, parts of dialogue may be present in the output video, but it may also skip some dialogue. This problem drove me crazy for two days. The file might look correct in the log, but it needs the \n in order to be formatted correctly for reading by FFmpeg.
function createAssSubtitleContent(subtitleSegments) {
let subtitleContent = `[Script Info]\n`;
subtitleContent += `ScriptType: v4.00+\n`;
subtitleContent += `PlayResX: 1920\n`;
subtitleContent += `PlayResY: 1080\n\n`;
subtitleContent += `[V4+ Styles]\n`;
subtitleContent += `Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n`;
subtitleContent += `Style: Default,Arial,72,&HFFFFFF,&HFFFFFF,&H000000,&H000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,40,0\n\n`;
subtitleContent += `[Events]\n`;
subtitleContent += `Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n`;
// Append each subtitle entry
subtitleSegments.forEach((segment) => {
const startTime = formatTime(segment.start);
const endTime = formatTime(segment.end);
const text = segment.text.trim();
subtitleContent += `Dialogue: 0,${startTime},${endTime},Default,,0,0,0,,${text}\n`;
});
return subtitleContent;
}
function formatTime(seconds) {
// Total centiseconds (100 centiseconds in a second)
const totalCentiseconds = Math.round(seconds * 100);
// Extract centiseconds
const cs = totalCentiseconds % 100;
// Total seconds
const totalSeconds = Math.floor(totalCentiseconds / 100);
// Extract seconds
const secs = totalSeconds % 60;
// Total minutes
const totalMinutes = Math.floor(totalSeconds / 60);
// Extract minutes
const mins = totalMinutes % 60;
// Hours
const hrs = Math.floor(totalMinutes / 60);
// Format with leading zeros where necessary
return `${hrs}:${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}.${String(cs).padStart(2, '0')}`; }
2. Ensure Proper Formatting with Newline Characters
Note the use of \n
to add newline characters after each line and between sections. This is crucial for the correct structure of the ASS file.
let subtitleContent = `[Script Info]\n`;
subtitleContent += `ScriptType: v4.00+\n`;
subtitleContent += `PlayResX: 1920\n`;
subtitleContent += `PlayResY: 1080\n\n`; // Double newline to separate sections
3. Write the Subtitle File Without BOM
When writing the ASS content to a file, ensure that you use UTF-8 encoding without a Byte Order Mark (BOM). Including a BOM can cause FFmpeg to fail in parsing the file.
import RNFS from 'react-native-fs';
export const createAssSubtitleFile = async (subtitleContent) => {
const filePath = `${RNFS.CachesDirectoryPath}/subtitles.ass`;
try {
// Write the file without BOM
await RNFS.writeFile(filePath, subtitleContent, 'utf8');
console.log(`Subtitle file successfully written to: ${filePath}`);
return filePath;
} catch (error) {
console.error('Failed to write subtitle file:', error);
return null;
}
};
4. Use Correct Encoding and Line Endings
Ensure that your subtitle file uses Unix-style line endings (\n
) consistently. Inconsistent line endings can cause parsing issues with FFmpeg.
Example Code
Putting it all together, here’s a complete example:
function formatTime(seconds) {
const totalCentiseconds = Math.round(seconds * 100);
const cs = totalCentiseconds % 100;
const totalSeconds = Math.floor(totalCentiseconds / 100);
const secs = totalSeconds % 60;
const totalMinutes = Math.floor(totalSeconds / 60);
const mins = totalMinutes % 60;
const hrs = Math.floor(totalMinutes / 60);
return `${hrs}:${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}.${String(cs).padStart(2, '0')}`;
}
function createAssSubtitleContent(subtitleSegments) {
let subtitleContent = `[Script Info]\n`;
subtitleContent += `ScriptType: v4.00+\n`;
subtitleContent += `PlayResX: 1920\n`;
subtitleContent += `PlayResY: 1080\n\n`;
subtitleContent += `[V4+ Styles]\n`;
subtitleContent += `Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n`;
subtitleContent += `Style: Default,Arial,72,&HFFFFFF,&HFFFFFF,&H000000,&H000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,40,0\n\n`;
subtitleContent += `[Events]\n`;
subtitleContent += `Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n`;
// Append each subtitle entry
subtitleSegments.forEach((segment) => {
const startTime = formatTime(segment.start);
const endTime = formatTime(segment.end);
const text = segment.text.trim();
subtitleContent += `Dialogue: 0,${startTime},${endTime},Default,,0,0,0,,${text}\n`;
});
return subtitleContent;
}
export const createAssSubtitleFile = async (subtitleContent) => {
const filePath = `${RNFS.CachesDirectoryPath}/subtitles.ass`;
try {
// Write the file without BOM
await RNFS.writeFile(filePath, subtitleContent, 'utf8');
console.log(`Subtitle file successfully written to: ${filePath}`);
return filePath;
} catch (error) {
console.error('Failed to write subtitle file:', error);
return null;
}
};
Testing the Subtitles with FFmpeg
When applying the subtitles to your video using FFmpeg, ensure that you correctly reference the subtitle file and handle any special characters in the file path.
const command = `-i "${videoUri}" ` +
`-vf "subtitles='${subtitleFile}'" ` +
`-c:v h264 -preset slow -crf 18 -c:a aac -b:a 128k "${outputPath}"`;
// Execute the FFmpeg command using your preferred method
const executeClipCommand = async (command, outputPath) => {
let success = false;
try {
const session = await FFmpegKit.execute(command);
const returnCode = await session.getReturnCode();
if (ReturnCode.isSuccess(returnCode)) {
success = true
} else {
console.error('Failed to process video segment',
await session.getFailStackTrace());
}
} catch (err) {
console.error('Error in processing video segment:', err);
}
if (success === true) {
return outputPath
}
}
Additional Tips
- Consistent Encoding: Always save your files with UTF-8 encoding without BOM.
- Inspect the Subtitle File: Open the generated ASS file in a text editor to ensure it’s formatted correctly.
- FFmpeg Logs: Remove
-loglevel quiet
from your FFmpeg command to see any errors or warnings that might help in troubleshooting.
Conclusion
By ensuring that your ASS subtitle files are correctly formatted with the necessary newline characters and proper encoding, you can avoid issues with missing subtitles when processing videos with FFmpeg in React Native. The key takeaway is to pay close attention to the structure of the ASS file and to validate the output whenever you make changes to the subtitle generation logic.
With the steps outlined in this guide, you should be able to generate ASS subtitle files effectively and integrate them seamlessly into your video processing workflow.
If you would like more information or examples of using FFmpeg in React Native, please leave a comment. Contact Newcolor for more.
Resource Links
Advanced SubStation Alpha (ASS) Subtitle Format
React Native FS (RNFS) Library