// --------------------------------------------------------------------------------------------------------------------
//
// This file is part of the HandBrake source code - It may be used under the terms of the GNU General Public License.
//
//
// Defines the HandBrakeUtils type.
//
// --------------------------------------------------------------------------------------------------------------------
namespace HandBrake.Interop
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using HandBrake.Interop.EventArgs;
using HandBrake.Interop.HbLib;
using HandBrake.Interop.Model;
using HandBrake.Interop.Model.Encoding;
using HandBrake.Interop.Model.Scan;
///
/// HandBrake Interop Utilities
///
public static class HandBrakeUtils
{
///
/// Estimated overhead in bytes for each frame in output container.
///
internal const int ContainerOverheadPerFrame = 6;
///
/// The callback for log messages from HandBrake.
///
private static LoggingCallback loggingCallback;
///
/// The callback for error messages from HandBrake.
///
private static LoggingCallback errorCallback;
///
/// True if the global initialize function has been called.
///
private static bool globalInitialized;
///
/// Fires when HandBrake has logged a message.
///
public static event EventHandler MessageLogged;
///
/// Fires when HandBrake has logged an error.
///
public static event EventHandler ErrorLogged;
///
/// Initializes static members of the HandBrakeUtils class.
///
static HandBrakeUtils()
{
if (!globalInitialized)
{
if (HBFunctions.hb_global_init() == -1)
{
throw new InvalidOperationException("HB global init failed.");
}
globalInitialized = true;
}
}
///
/// Gets the HandBrake version string.
///
public static string Version
{
get
{
var versionPtr = HBFunctions.hb_get_version(IntPtr.Zero); // Pointer isn't actually used.
return Marshal.PtrToStringAnsi(versionPtr);
}
}
///
/// Gets the HandBrake build number.
///
public static int Build
{
get
{
return HBFunctions.hb_get_build(IntPtr.Zero);
}
}
///
/// Ensures the HB global initialize method has been called.
///
public static void EnsureGlobalInit()
{
// Does nothing, but invokes static ctor.
}
///
/// Enables or disables LibDVDNav. If disabled libdvdread will be used instead.
///
///
/// True to enable LibDVDNav.
///
public static void SetDvdNav(bool enableDvdNav)
{
HBFunctions.hb_dvd_set_dvdnav(enableDvdNav ? 1 : 0);
}
///
/// Call before app shutdown. Performs global cleanup.
///
public static void DisposeGlobal()
{
HBFunctions.hb_global_close();
}
///
/// Register the logger.
///
public static void RegisterLogger()
{
// Register the logger if we have not already
if (loggingCallback == null)
{
// Keep the callback as a member to prevent it from being garbage collected.
loggingCallback = LoggingHandler;
errorCallback = ErrorHandler;
HBFunctions.hb_register_logger(loggingCallback);
HBFunctions.hb_register_error_handler(errorCallback);
}
}
///
/// Handles log messages from HandBrake.
///
///
/// The log message (including newline).
///
public static void LoggingHandler(string message)
{
if (!string.IsNullOrEmpty(message))
{
string[] messageParts = message.Split(new[] { "\n" }, StringSplitOptions.RemoveEmptyEntries);
if (messageParts.Length > 0)
{
message = messageParts[0];
SendMessageEvent(message);
}
}
}
///
/// Handles errors from HandBrake.
///
///
/// The error message.
///
public static void ErrorHandler(string message)
{
if (!string.IsNullOrEmpty(message))
{
// These errors happen in normal operations. Log them as messages.
if (message == "dvd: ifoOpen failed" || message.Contains("avformat_seek_file failed") || message.Contains("nav_get_title_list"))
{
SendMessageEvent(message);
return;
}
SendErrorEvent(message);
}
}
///
/// Gets the standard x264 option name given the starting point.
///
///
/// The name.
///
///
/// The standard x264 option name.
///
public static string SanitizeX264OptName(string name)
{
IntPtr namePtr = Marshal.StringToHGlobalAnsi(name);
string sanitizedName = Marshal.PtrToStringAnsi(HBFunctions.hb_x264_encopt_name(namePtr));
Marshal.FreeHGlobal(namePtr);
return sanitizedName;
}
///
/// Checks to see if the given H.264 level is valid given the inputs.
///
///
/// The level to check.
///
///
/// The output picture width.
///
///
/// The output picture height.
///
///
/// The rate numerator.
///
///
/// The rate denominator.
///
///
/// True if x264 interlaced output is enabled.
///
///
/// True if x264 fake interlacing is enabled.
///
///
/// True if the level is valid.
///
public static bool IsH264LevelValid(string level, int width, int height, int fpsNumerator, int fpsDenominator, bool interlaced, bool fakeInterlaced)
{
return HBFunctions.hb_check_h264_level(
level,
width,
height,
fpsNumerator,
fpsDenominator,
interlaced ? 1 : 0,
fakeInterlaced ? 1 : 0) == 0;
}
///
/// Creates an X264 options string from the given settings.
///
///
/// The x264 preset.
///
///
/// The x264 tunes being used.
///
///
/// The extra options string.
///
///
/// The H.264 profile.
///
///
/// The H.264 level.
///
///
/// The width of the final picture.
///
///
/// The height of the final picture.
///
///
/// The full x264 options string from the given inputs.
///
public static string CreateX264OptionsString(
string preset,
IList tunes,
string extraOptions,
string profile,
string level,
int width,
int height)
{
if (width <= 0)
{
throw new ArgumentException("width must be positive.");
}
if (height <= 0)
{
throw new ArgumentException("height must be positive.");
}
IntPtr ptr = HBFunctions.hb_x264_param_unparse(
preset,
string.Join(",", tunes),
extraOptions,
profile,
level,
width,
height);
string x264Settings = Marshal.PtrToStringAnsi(ptr);
return x264Settings;
}
///
/// Gets the total number of seconds on the given encode job.
///
///
/// The encode job to query.
///
///
/// The title being encoded.
///
///
/// The total number of seconds of video to encode.
///
internal static double GetJobLengthSeconds(EncodeJob job, Title title)
{
switch (job.RangeType)
{
// case VideoRangeType.All:
// return title.Duration.TotalSeconds;
case VideoRangeType.Chapters:
TimeSpan duration = TimeSpan.Zero;
for (int i = job.ChapterStart; i <= job.ChapterEnd; i++)
{
duration += title.Chapters[i - 1].Duration;
}
return duration.TotalSeconds;
case VideoRangeType.Seconds:
return job.SecondsEnd - job.SecondsStart;
case VideoRangeType.Frames:
return (job.FramesEnd - job.FramesStart) / title.Framerate;
}
return 0;
}
///
/// Gets the number of audio samples used per frame for the given audio encoder.
///
///
/// The encoder to query.
///
///
/// The number of audio samples used per frame for the given
/// audio encoder.
///
internal static int GetAudioSamplesPerFrame(string encoderName)
{
switch (encoderName)
{
case "faac":
case "ffaac":
case "copy:aac":
case "vorbis":
return 1024;
case "lame":
case "copy:mp3":
return 1152;
case "ffac3":
case "copy":
case "copy:ac3":
case "copy:dts":
case "copy:dtshd":
return 1536;
}
// Unknown encoder; make a guess.
return 1536;
}
///
/// Gets the size in bytes for the audio with the given parameters.
///
///
/// The encode job.
///
///
/// The length of the encode in seconds.
///
///
/// The title to encode.
///
///
/// The list of tracks to encode.
///
///
/// The size in bytes for the audio with the given parameters.
///
internal static long GetAudioSize(EncodeJob job, double lengthSeconds, Title title, List> outputTrackList)
{
long audioBytes = 0;
foreach (Tuple outputTrack in outputTrackList)
{
AudioEncoding encoding = outputTrack.Item1;
AudioTrack track = title.AudioTracks[outputTrack.Item2 - 1];
int samplesPerFrame = GetAudioSamplesPerFrame(encoding.Encoder);
int audioBitrate;
HBAudioEncoder audioEncoder = HandBrakeEncoderHelpers.GetAudioEncoder(encoding.Encoder);
if (audioEncoder.IsPassthrough)
{
// Input bitrate is in bits/second.
audioBitrate = track.Bitrate / 8;
}
else if (encoding.EncodeRateType == AudioEncodeRateType.Quality)
{
// Can't predict size of quality targeted audio encoding.
audioBitrate = 0;
}
else
{
int outputBitrate;
if (encoding.Bitrate > 0)
{
outputBitrate = encoding.Bitrate;
}
else
{
outputBitrate = HandBrakeEncoderHelpers.GetDefaultBitrate(
audioEncoder,
encoding.SampleRateRaw == 0 ? track.SampleRate : encoding.SampleRateRaw,
HandBrakeEncoderHelpers.SanitizeMixdown(HandBrakeEncoderHelpers.GetMixdown(encoding.Mixdown), audioEncoder, track.ChannelLayout));
}
// Output bitrate is in kbps.
audioBitrate = outputBitrate * 1000 / 8;
}
audioBytes += (long)(lengthSeconds * audioBitrate);
// Audio overhead
audioBytes += encoding.SampleRateRaw * ContainerOverheadPerFrame / samplesPerFrame;
}
return audioBytes;
}
///
/// Calculates the video bitrate for the given job and target size.
///
///
/// The encode job.
///
///
/// The title.
///
///
/// The target size in MB.
///
///
/// The currently selected encode length. Used in preview
/// for calculating bitrate when the target size would be wrong.
///
///
/// The video bitrate in kbps.
///
public static int CalculateBitrate(EncodeJob job, Title title, int sizeMB, double overallSelectedLengthSeconds = 0)
{
long availableBytes = ((long)sizeMB) * 1024 * 1024;
EncodingProfile profile = job.EncodingProfile;
double lengthSeconds = overallSelectedLengthSeconds > 0 ? overallSelectedLengthSeconds : GetJobLengthSeconds(job, title);
lengthSeconds += 1.5;
double outputFramerate;
if (profile.Framerate == 0)
{
outputFramerate = title.Framerate;
}
else
{
// Not sure what to do for VFR here hb_calc_bitrate never handled it...
// just use the peak for now.
outputFramerate = profile.Framerate;
}
long frames = (long)(lengthSeconds * outputFramerate);
availableBytes -= frames * ContainerOverheadPerFrame;
List> outputTrackList = GetOutputTracks(job, title);
availableBytes -= GetAudioSize(job, lengthSeconds, title, outputTrackList);
if (availableBytes < 0)
{
return 0;
}
// Video bitrate is in kilobits per second, or where 1 kbps is 1000 bits per second.
// So 1 kbps is 125 bytes per second.
return (int)(availableBytes / (125 * lengthSeconds));
}
///
/// Gives estimated file size (in MB) of the given job and video bitrate.
///
///
/// The encode job.
///
///
/// The title.
///
///
/// The video bitrate to be used (kbps).
///
///
/// The estimated file size (in MB) of the given job and video bitrate.
///
public static double CalculateFileSize(EncodeJob job, Title title, int videoBitrate)
{
long totalBytes = 0;
EncodingProfile profile = job.EncodingProfile;
double lengthSeconds = GetJobLengthSeconds(job, title);
lengthSeconds += 1.5;
double outputFramerate;
if (profile.Framerate == 0)
{
outputFramerate = title.Framerate;
}
else
{
// Not sure what to do for VFR here hb_calc_bitrate never handled it...
// just use the peak for now.
outputFramerate = profile.Framerate;
}
long frames = (long)(lengthSeconds * outputFramerate);
totalBytes += (long)(lengthSeconds * videoBitrate * 125);
totalBytes += frames * ContainerOverheadPerFrame;
List> outputTrackList = GetOutputTracks(job, title);
totalBytes += GetAudioSize(job, lengthSeconds, title, outputTrackList);
return (double)totalBytes / 1024 / 1024;
}
///
/// Sends the message logged event to any registered listeners.
///
///
/// The message to send.
///
private static void SendMessageEvent(string message)
{
if (MessageLogged != null)
{
MessageLogged(null, new MessageLoggedEventArgs { Message = message });
}
Debug.WriteLine(message);
}
///
/// Sends the error logged event to any registered listeners.
///
///
/// The message to send
///
private static void SendErrorEvent(string message)
{
if (ErrorLogged != null)
{
ErrorLogged(null, new MessageLoggedEventArgs { Message = message });
}
Debug.WriteLine("ERROR: " + message);
}
///
/// Gets a list of encodings and target track indices (1-based).
///
/// The encode job
/// The title the job is meant to encode.
/// A list of encodings and target track indices (1-based).
private static List> GetOutputTracks(EncodeJob job, Title title)
{
var list = new List>();
foreach (AudioEncoding encoding in job.EncodingProfile.AudioEncodings)
{
if (encoding.InputNumber == 0)
{
// Add this encoding for all chosen tracks
foreach (int chosenTrack in job.ChosenAudioTracks)
{
// In normal cases we'll never have a chosen audio track that doesn't exist but when batch encoding
// we just choose the first audio track without checking if it exists.
if (chosenTrack <= title.AudioTracks.Count)
{
list.Add(new Tuple(encoding, chosenTrack));
}
}
}
else if (encoding.InputNumber <= job.ChosenAudioTracks.Count)
{
// Add this encoding for the specified track, if it exists
int trackNumber = job.ChosenAudioTracks[encoding.InputNumber - 1];
// In normal cases we'll never have a chosen audio track that doesn't exist but when batch encoding
// we just choose the first audio track without checking if it exists.
if (trackNumber <= title.AudioTracks.Count)
{
list.Add(new Tuple(encoding, trackNumber));
}
}
}
return list;
}
}
}