DVD with Subtitles

This topic describes the steps needed to create a DVD from an MP4 video.

Project Directory

You need a directory to store the source video and the DVDBuilder project. This can be anywhere on your machine. For example create C:\SimpleDVDWithSubtitles directory. We will refer to that as the DVD Project Directory.

Source Video

Download the file A Música Irresistivel de Benny Goodman 1956.mp4 from the Internet Archive to the DVD Project Directory.

Rename the file from A Música Irresistivel de Benny Goodman 1956.mp4 to movie.mp4.

Subtitle Files

Download the DVDBuilder-Samples repository. Unzip the downloaded file. Copy french, portuguese and spanish directories from projects\SimpleDVDWithSubtitles to DVD Project Directory.

Convert Source Video To MPEG-2

DVDBuilder accepts only MPEG-2 Program Stream as input, but you can use AVBlocks to convert the source video from MP4 to MPG (MPEG-2 Program Stream). AVBlocks is already included with DVDBuilder.


string sourceMp4 = Path.Combine(DVDProjectDirectory, "movie.mp4");
string sourceMpeg2 = Path.Combine(DVDProjectDirectory, "movie.mpg");

ConvertSourceToMpeg2(sourceMp4, sourceMpeg2);

using avb = PrimoSoftware.AVBlocks;

private static void ConvertSourceToMpeg2(string inputFile, string outputFile)
{
    if (File.Exists(outputFile))
        File.Delete(outputFile);

    var inputInfo = new avb.MediaInfo() {
        InputFile = inputFile
    };

    if (inputInfo.Load())
    {
        var inputSocket = avb.MediaSocket.FromMediaInfo(inputInfo);

        var outputSocket = 
            avb.MediaSocket.FromPreset(avb.Preset.Video.DVD.NTSC_16x9_PCM);

        outputSocket.File = outputFile;

        using (var transcoder = new avb.Transcoder())
        {
            // setup
            transcoder.AllowDemoMode = true;

            transcoder.Inputs.Add(inputSocket);
            transcoder.Outputs.Add(outputSocket);

            // handle progress event to report progress
            transcoder.OnProgress += (sender, args) => {
                DrawTextProgressBar((int)args.CurrentTime, (int)args.TotalTime);
            };

            // handle continue event to stop transcoding
            transcoder.OnContinue += (sender, args) => {
                // stop transcoder after 3 minutes and 20 seconds
                if (args.CurrentTime >= 3 * 60 + 20)
                    args.Continue = false;
            };

            if (transcoder.Open())
            {
                transcoder.Run();
                transcoder.Close();
            }
        }
    }
}

Encode subtitles


// Spanish
Console.WriteLine("  Spanish \n");
string subtitle1 = Path.Combine(DVDProjectDirectory, "spanish/spanish.stl");
string encodedSubtitle1 = Path.Combine(DVDProjectDirectory, "spanish_sub.mpg");

EncodeSubtitle(subtitle1, encodedSubtitle1);

// French
Console.WriteLine("  French \n");
string subtitle2 = Path.Combine(DVDProjectDirectory, "french/french.stl");
string encodedSubtitle2 = Path.Combine(DVDProjectDirectory, "french_sub.mpg");

EncodeSubtitle(subtitle2, encodedSubtitle2);

// Portuguese 
Console.WriteLine("  Portuguese \n");
string subtitle3 = Path.Combine(DVDProjectDirectory, "portuguese/portuguese.stl");
string encodedSubtitle3 = Path.Combine(DVDProjectDirectory, "portuguese_sub.mpg");

EncodeSubtitle(subtitle3, encodedSubtitle3);

private static void EncodeSubtitle(string subtitle, string encodedSubtitle)
{
    if (File.Exists(encodedSubtitle))
        File.Delete(encodedSubtitle);

    using (var subpicEncoder = new dvdb.SubpictureEncoder())
    {
        if (!subpicEncoder.Encode(subtitle, encodedSubtitle))
        {
            Console.WriteLine(subpicEncoder.Error.Message);
        }
    }
}

Create DVDBuilder Project

DVDBuilder project is an XML file that describes a video DVD.

In the DVD Project Directory create a file project.xml with the following contents:


<?xml version='1.0' encoding='utf-8'?>
<dvd version='2.3' xmlns='http://www.primosoftware.com/dvdbuilder/2.3'>
    <videoManager firstPlayNavigate='Title=1'/>
    <titleSet>
        <subpictureStreams>
            <!--
            Declare subtitle streams for the DVD menu
            Actual streams will be passed in the videoObject of each title below
            -->
            <!-- Spanish, French and Portuguese subtitles -->
            <stream languageCode="ES" mpegStreamID="0xBD" mpegSubstreamID="0x20"/>
            <stream languageCode="FR" mpegStreamID="0xBD" mpegSubstreamID="0x21"/>
            <stream languageCode="PT" mpegStreamID="0xBD" mpegSubstreamID="0x22"/>
        </subpictureStreams>
        <titles>
            <title id='1' chapters='00:00:00;'>
                <videoObject file='movie.mpg'>
                    <!--
                    Add subtitle streams. The order and streams must match the streams from the titleSet above:
                        spanish_sub.mpg   - encoded from spanish/spanish.stl
                        french_sub.mpg    - encoded from french/french.stl
                        portuguese_sub.mpg - encoded from portuguese/portuguese.stl 
                    -->
                    <subpictureStream file='spanish_sub.mpg' />
                    <subpictureStream file='french_sub.mpg' />
                    <subpictureStream file='portuguese_sub.mpg' />
                </videoObject>
                <subpicturePalette>
                    <!-- Not used - black -->
                    <color index="0" value="#000000" />
                    <!-- Text color - white -->
                    <color index="1" value="#FFFFFF" />
                    <!-- Text outer outline color / outline 1 - off-black -->
                    <color index="2" value="#202020" />
                    <!-- Text inner outline color / outline 2 - gray -->
                    <color index="3" value="#808080" />
                    <!-- 
                    Text background color - blue
                    This should not be visible unless you set $BackgroundContrast to > 0 in the source STL file 
                    -->
                    <color index="4" value="#0000FF" />
                </subpicturePalette>
            </title>
        </titles>
    </titleSet>
</dvd>

Build DVD Structures

Simply create a DVDBuilder object, set the ProjectFile and OutputFolder properties, and call the Build method.


string project = Path.Combine(DVDProjectDirectory, "project.xml");
string dvdFiles = Path.Combine(DVDProjectDirectory, "dvd");

BuildDVDStructures(project, dvdFiles);

using dvdb = PrimoSoftware.DVDBuilder;

private static void BuildDVDStructures(string project, string dvdFiles)
{
    if (Directory.Exists(dvdFiles))
        Directory.Delete(dvdFiles, true);

    using (var dvdBuilder = new dvdb.DVDBuilder())
    {
        // handle progress event to report progress
        dvdBuilder.OnProgress += (sender, args) => {
            DrawTextProgressBar(args.Percent, 100, "%");
        };

        dvdBuilder.ProjectFile = project;
        dvdBuilder.OutputFolder = dvdFiles;

        if (!dvdBuilder.Build())
        {
            Console.WriteLine(dvdBuilder.Error.Message);
        }
    }
}

Complete C# Code

Here is the complete code.


using System;
using System.IO;

using avb = PrimoSoftware.AVBlocks;
using dvdb = PrimoSoftware.DVDBuilder;

namespace SimpleDVDWithSubtitles
{
    class Program
    {
        const string DVDProjectDirectory = "C:/SimpleDVDWithSubtitles";

        // 3 minutes and 20 seconds
        const int clipTimeSeconds = 3 * 60 + 20; 

        static void Main(string[] args)
        {
            // Video encoding
            PrimoSoftware.AVBlocks.Library.Initialize();
            PrimoSoftware.AVBlocks.Library.SetLicense("license_xml_string");

            // DVD authoring
            PrimoSoftware.DVDBuilder.Library.Initialize();
            PrimoSoftware.DVDBuilder.Library.SetLicense("license_xml_string");

            // Step 1: Convert source video
            Console.WriteLine("Converting source video ... \n");

            string sourceMp4 = Path.Combine(DVDProjectDirectory, "movie.mp4");
            string sourceMpeg2 = Path.Combine(DVDProjectDirectory, "movie.mpg");

            ConvertSourceToMpeg2(sourceMp4, sourceMpeg2);

            // Step 2: Encode subtitles
            Console.WriteLine("\n\n Encoding subtitles ... \n");

            // Spanish
            Console.WriteLine("  Spanish \n");
            string subtitle1 = Path.Combine(DVDProjectDirectory, "spanish/spanish.stl");
            string encodedSubtitle1 = Path.Combine(DVDProjectDirectory, "spanish_sub.mpg");

            EncodeSubtitle(subtitle1, encodedSubtitle1);

            // French
            Console.WriteLine("  French \n");
            string subtitle2 = Path.Combine(DVDProjectDirectory, "french/french.stl");
            string encodedSubtitle2 = Path.Combine(DVDProjectDirectory, "french_sub.mpg");

            EncodeSubtitle(subtitle2, encodedSubtitle2);

            // Portuguese 
            Console.WriteLine("  Portuguese \n");
            string subtitle3 = Path.Combine(DVDProjectDirectory, "portuguese/portuguese.stl");
            string encodedSubtitle3 = Path.Combine(DVDProjectDirectory, "portuguese_sub.mpg");

            EncodeSubtitle(subtitle3, encodedSubtitle3);

            // Step 3: Build DVD structure
            Console.WriteLine("Building DVD structure ... \n");

            string project = Path.Combine(DVDProjectDirectory, "project.xml");
            string dvdFiles = Path.Combine(DVDProjectDirectory, "dvd");

            BuildDVDStructures(project, dvdFiles);

            PrimoSoftware.DVDBuilder.Library.Shutdown();
            PrimoSoftware.AVBlocks.Library.Shutdown();
        }

        private static void ConvertSourceToMpeg2(string inputFile, string outputFile)
        {
            if (File.Exists(outputFile))
                File.Delete(outputFile);

            var inputInfo = new avb.MediaInfo() {
                InputFile = inputFile
            };

            if (inputInfo.Load())
            {
                var inputSocket = avb.MediaSocket.FromMediaInfo(inputInfo);

                var outputSocket = 
                    avb.MediaSocket.FromPreset(avb.Preset.Video.DVD.NTSC_16x9_PCM);

                outputSocket.File = outputFile;

                using (var transcoder = new avb.Transcoder())
                {
                    // setup
                    transcoder.AllowDemoMode = true;

                    transcoder.Inputs.Add(inputSocket);
                    transcoder.Outputs.Add(outputSocket);

                    // handle progress event to report progress
                    transcoder.OnProgress += (sender, args) => {
                        DrawTextProgressBar((int)args.CurrentTime, clipTimeSeconds, "seconds");
                    };

                    // handle continue event to stop transcoding
                    transcoder.OnContinue += (sender, args) => {
                        // stop transcoder after 3 minutes and 20 seconds
                        if (args.CurrentTime >= clipTimeSeconds + 1)
                            args.Continue = false;
                    };

                    if (transcoder.Open())
                    {
                        transcoder.Run();
                        transcoder.Close();
                    }
                }
            }
        }

        private static void EncodeSubtitle(string subtitle, string encodedSubtitle)
        {
            if (File.Exists(encodedSubtitle))
                File.Delete(encodedSubtitle);

            using (var subpicEncoder = new dvdb.SubpictureEncoder())
            {
                if (!subpicEncoder.Encode(subtitle, encodedSubtitle))
                {
                    Console.WriteLine(subpicEncoder.Error.Message);
                }
            }
        }

        private static void BuildDVDStructures(string project, string dvdFiles)
        {
            if (Directory.Exists(dvdFiles))
                Directory.Delete(dvdFiles, true);

            using (var dvdBuilder = new dvdb.DVDBuilder())
            {
                // handle progress event to report progress
                dvdBuilder.OnProgress += (sender, args) => {
                    DrawTextProgressBar(args.Percent, 100, "%");
                };

                dvdBuilder.ProjectFile = project;
                dvdBuilder.OutputFolder = dvdFiles;

                if (!dvdBuilder.Build())
                {
                    Console.WriteLine(dvdBuilder.Error.Message);
                }
            }
        }

        private static void DrawTextProgressBar(int progress, int total, string units)
        {
            // draw empty progress bar
            Console.CursorLeft = 0;
            Console.Write("["); //start
            Console.CursorLeft = 32;
            Console.Write("]"); //end
            Console.CursorLeft = 1;
            float oneChunk = 30.0f / total;

            // draw filled part
            int position = 1;
            for (int i = 0; i < oneChunk * progress; i++)
            {
                Console.BackgroundColor = ConsoleColor.Gray;
                Console.CursorLeft = position++;
                Console.Write(" ");
            }

            // draw unfilled part
            for (int i = position; i <= 31; i++)
            {
                Console.BackgroundColor = ConsoleColor.Green;
                Console.CursorLeft = position++;
                Console.Write(" ");
            }

            // draw totals
            Console.CursorLeft = 35;
            Console.BackgroundColor = ConsoleColor.Black;
            Console.Write(progress.ToString() + " of " + total.ToString() + " " + units); // blanks at the end remove any excess
        }
    }
}


Last updated on November 10th, 2017 12:00:00 AM