mirror of https://github.com/HandBrake/HandBrake
627 lines
15 KiB
Objective-C
627 lines
15 KiB
Objective-C
/* HBTitle.m $
|
|
|
|
This file is part of the HandBrake source code.
|
|
Homepage: <http://handbrake.fr/>.
|
|
It may be used under the terms of the GNU General Public License. */
|
|
|
|
#import "HBTitle.h"
|
|
#import "HBTitle+Private.h"
|
|
#import "HBChapter.h"
|
|
#import "HBPreset.h"
|
|
#import "NSDictionary+HBAdditions.h"
|
|
#import "HBLocalizationUtilities.h"
|
|
#import "HBCodingUtilities.h"
|
|
#import "HBSecurityAccessToken.h"
|
|
#import "HBUtilities.h"
|
|
|
|
#include "handbrake/lang.h"
|
|
|
|
@interface HBMetadata ()
|
|
|
|
@property (nonatomic, readonly, nullable) hb_metadata_t *metadata;
|
|
@property (nonatomic, copy, nullable) NSString *releaseDate;
|
|
|
|
@end
|
|
|
|
@implementation HBMetadata
|
|
- (instancetype)initWithMetadata:(hb_metadata_t *)metadata
|
|
{
|
|
self = [super init];
|
|
if (self)
|
|
{
|
|
_metadata = metadata;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (NSString *)releaseDate
|
|
{
|
|
if (_metadata && _metadata->dict)
|
|
{
|
|
const char *releaseDate = hb_dict_get_string(_metadata->dict, "ReleaseDate");
|
|
if (releaseDate)
|
|
{
|
|
return @(releaseDate);
|
|
}
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation HBTitleAudioTrack
|
|
|
|
- (instancetype)initWithDisplayName:(NSString *)displayName
|
|
{
|
|
self = [super init];
|
|
if (self)
|
|
{
|
|
_index = -1;
|
|
_displayName = [displayName copy];
|
|
_title = @"";
|
|
_isoLanguageCode = @"";
|
|
_chLayout = @"";
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (instancetype)initWithAudioTrack:(hb_audio_config_t *)audio index:(int)index
|
|
{
|
|
self = [super init];
|
|
if (self)
|
|
{
|
|
_index = audio->index;
|
|
_displayName = [NSString stringWithFormat: @"%d: %@", index, @(audio->lang.description)];
|
|
_title = audio->in.name ? @(audio->in.name) : nil;
|
|
_bitRate = audio->in.bitrate / 1000;
|
|
_sampleRate = audio->in.samplerate;
|
|
_codec = audio->in.codec;
|
|
_codecParam = audio->in.codec_param;
|
|
|
|
char channelLayoutDescription[256];
|
|
hb_layout_get_name(audio->in.ch_layout, channelLayoutDescription, sizeof(channelLayoutDescription));
|
|
_chLayout = @(channelLayoutDescription);
|
|
|
|
_isoLanguageCode = @(audio->lang.iso639_2);
|
|
}
|
|
return self;
|
|
}
|
|
|
|
+ (BOOL)supportsSecureCoding { return YES; }
|
|
|
|
- (void)encodeWithCoder:(nonnull NSCoder *)coder
|
|
{
|
|
encodeInt(_index);
|
|
encodeObject(_displayName);
|
|
encodeObject(_title);
|
|
encodeInt(_bitRate);
|
|
encodeInt(_sampleRate);
|
|
encodeInt(_codec);
|
|
encodeInt(_codecParam);
|
|
encodeObject(_chLayout);
|
|
encodeObject(_isoLanguageCode);
|
|
}
|
|
|
|
- (nullable instancetype)initWithCoder:(nonnull NSCoder *)decoder
|
|
{
|
|
self = [super init];
|
|
if (self)
|
|
{
|
|
decodeInt(_index);
|
|
decodeObjectOrFail(_displayName, NSString);
|
|
decodeObject(_title, NSString);
|
|
decodeInt(_bitRate);
|
|
decodeInt(_sampleRate);
|
|
decodeInt(_codec);
|
|
decodeInt(_codecParam);
|
|
decodeObjectOrFail(_chLayout, NSString);
|
|
decodeObjectOrFail(_isoLanguageCode, NSString);
|
|
}
|
|
return self;
|
|
fail:
|
|
return nil;
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
@interface HBTitleSubtitlesTrack ()
|
|
|
|
@property (nonatomic, readonly, nullable) NSData *bookmark;
|
|
|
|
@property (nonatomic, readwrite) NSInteger accessCount;
|
|
@property (nonatomic, readwrite) HBSecurityAccessToken *fileURLToken;
|
|
|
|
@end
|
|
|
|
@implementation HBTitleSubtitlesTrack
|
|
|
|
- (instancetype)initWithDisplayName:(NSString *)displayName type:(int)type fileURL:(nullable NSURL *)fileURL
|
|
{
|
|
self = [super init];
|
|
if (self)
|
|
{
|
|
_index = -1;
|
|
_displayName = [displayName copy];
|
|
_type = type;
|
|
_isoLanguageCode = @"und";
|
|
_fileURL = fileURL;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (instancetype)initWithSubtitlesTrack:(hb_subtitle_t *)subtitle index:(int)index
|
|
{
|
|
self = [super init];
|
|
if (self)
|
|
{
|
|
_index = index;
|
|
_displayName = [NSString stringWithFormat:@"%d: %@", index, @(subtitle->lang)];
|
|
_title = subtitle->name ? @(subtitle->name) : nil;
|
|
_type = subtitle->source;
|
|
_isoLanguageCode = @(subtitle->iso639_2);
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)refreshSecurityScopedResources
|
|
{
|
|
if (_bookmark)
|
|
{
|
|
NSURL *resolvedURL = [HBUtilities URLFromBookmark:_bookmark];
|
|
if (resolvedURL)
|
|
{
|
|
_fileURL = resolvedURL;
|
|
}
|
|
}
|
|
}
|
|
|
|
- (BOOL)startAccessingSecurityScopedResource
|
|
{
|
|
#ifdef __SANDBOX_ENABLED__
|
|
if (self.accessCount == 0)
|
|
{
|
|
if (_fileURL)
|
|
{
|
|
self.fileURLToken = [HBSecurityAccessToken tokenWithObject:_fileURL];
|
|
}
|
|
}
|
|
self.accessCount += 1;
|
|
return YES;
|
|
#else
|
|
return NO;
|
|
#endif
|
|
}
|
|
|
|
- (void)stopAccessingSecurityScopedResource
|
|
{
|
|
#ifdef __SANDBOX_ENABLED__
|
|
self.accessCount -= 1;
|
|
NSAssert(self.accessCount >= 0, @"[HBSubtitles stopAccessingSecurityScopedResource:] unbalanced call");
|
|
if (self.accessCount == 0)
|
|
{
|
|
self.fileURLToken = nil;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
+ (BOOL)supportsSecureCoding { return YES ;}
|
|
|
|
- (void)encodeWithCoder:(nonnull NSCoder *)coder
|
|
{
|
|
encodeInt(_index);
|
|
encodeObject(_displayName);
|
|
encodeObject(_title);
|
|
encodeInt(_type);
|
|
encodeObject(_isoLanguageCode);
|
|
#ifdef __SANDBOX_ENABLED__
|
|
if (_fileURL)
|
|
{
|
|
if (!_bookmark)
|
|
{
|
|
_bookmark = [HBUtilities bookmarkFromURL:_fileURL
|
|
options:NSURLBookmarkCreationWithSecurityScope | NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess];
|
|
}
|
|
encodeObject(_bookmark);
|
|
}
|
|
#endif
|
|
encodeObject(_fileURL);
|
|
}
|
|
|
|
- (nullable instancetype)initWithCoder:(nonnull NSCoder *)decoder
|
|
{
|
|
self = [super init];
|
|
if (self)
|
|
{
|
|
decodeInt(_index);
|
|
decodeObjectOrFail(_displayName, NSString);
|
|
decodeObject(_title, NSString);
|
|
decodeInt(_type);
|
|
decodeObjectOrFail(_isoLanguageCode, NSString);
|
|
|
|
#ifdef __SANDBOX_ENABLED__
|
|
decodeObject(_bookmark, NSData);
|
|
|
|
if (_bookmark)
|
|
{
|
|
decodeObjectOrFail(_fileURL, NSURL);
|
|
}
|
|
#else
|
|
decodeObject(_fileURL, NSURL);
|
|
#endif
|
|
}
|
|
return self;
|
|
fail:
|
|
return nil;
|
|
}
|
|
|
|
@end
|
|
|
|
@interface HBTitle ()
|
|
|
|
@property (nonatomic, readonly) hb_title_t *hb_title;
|
|
@property (nonatomic, readonly) hb_handle_t *hb_handle;
|
|
@property (nonatomic, readwrite, copy) NSString *name;
|
|
@property (nonatomic, readwrite) HBMetadata *metadata;
|
|
@property (nonatomic, readwrite) NSArray *audioTracks;
|
|
@property (nonatomic, readwrite) NSArray<HBTitleSubtitlesTrack *> *subtitlesTracks;
|
|
@property (nonatomic, readwrite) NSArray<HBChapter *> *chapters;
|
|
|
|
@end
|
|
|
|
@implementation HBTitle
|
|
|
|
- (instancetype)initWithTitle:(hb_title_t *)title handle:(hb_handle_t *)handle featured:(BOOL)featured
|
|
{
|
|
self = [super init];
|
|
if (self)
|
|
{
|
|
if (!title)
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
_hb_title = title;
|
|
_hb_handle = handle;
|
|
_featured = featured;
|
|
|
|
_metadata = [[HBMetadata alloc] initWithMetadata:title->metadata];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (NSString *)name
|
|
{
|
|
if (!_name)
|
|
{
|
|
_name = @(self.hb_title->name);
|
|
|
|
// Use the bundle name for eyetv
|
|
NSURL *parentURL = self.url.URLByDeletingLastPathComponent;
|
|
if ([parentURL.pathExtension caseInsensitiveCompare:@"eyetv"] == NSOrderedSame)
|
|
{
|
|
_name = parentURL.URLByDeletingPathExtension.lastPathComponent;
|
|
}
|
|
|
|
// If the name is empty use file/directory name
|
|
if (_name.length == 0)
|
|
{
|
|
_name = @(self.hb_title->path).lastPathComponent;
|
|
}
|
|
}
|
|
|
|
return _name;
|
|
}
|
|
|
|
- (BOOL)isStream
|
|
{
|
|
return (self.hb_title->type == HB_STREAM_TYPE || self.hb_title->type == HB_FF_STREAM_TYPE);
|
|
}
|
|
|
|
- (NSString *)description
|
|
{
|
|
if (self.hb_title->type == HB_BD_TYPE)
|
|
{
|
|
return [NSString stringWithFormat:@"%d - %@ - %05d.MPLS",
|
|
self.hb_title->index, self.timeCode, self.hb_title->playlist];
|
|
}
|
|
else if (self.hb_title->type == HB_DVD_TYPE)
|
|
{
|
|
return [NSString stringWithFormat:@"%d - %@",
|
|
self.hb_title->index, self.timeCode];
|
|
}
|
|
else
|
|
{
|
|
return [NSString stringWithFormat:@"%d - %@ - %@",
|
|
self.hb_title->index, self.timeCode, @(self.hb_title->name)];
|
|
}
|
|
}
|
|
|
|
- (NSString *)shortFormatDescription
|
|
{
|
|
NSMutableString *format = [[NSMutableString alloc] init];
|
|
|
|
[format appendFormat:@"%dx%d", _hb_title->geometry.width, _hb_title->geometry.height];
|
|
|
|
if (_hb_title->geometry.par.num != 1 || _hb_title->geometry.par.den != 1)
|
|
{
|
|
[format appendFormat:@" (%dx%d)", _hb_title->geometry.width * _hb_title->geometry.par.num / _hb_title->geometry.par.den,
|
|
_hb_title->geometry.height];
|
|
}
|
|
|
|
[format appendString:@", "];
|
|
|
|
NSString *fps = [NSString localizedStringWithFormat:HBKitLocalizedString(@"%.6g FPS", @"Title short description -> video format"), _hb_title->vrate.num / (double)_hb_title->vrate.den];
|
|
[format appendString:fps];
|
|
|
|
NSString *dynamicRange = @"SDR";
|
|
|
|
if (_hb_title->hdr_10_plus)
|
|
{
|
|
dynamicRange = @"HDR10+";
|
|
}
|
|
else if (_hb_title->mastering.has_primaries && _hb_title->mastering.has_luminance)
|
|
{
|
|
dynamicRange = @"HDR10";
|
|
}
|
|
else if (_hb_title->color_transfer == 16 || _hb_title->color_transfer == 18)
|
|
{
|
|
dynamicRange = @"HDR";
|
|
}
|
|
|
|
if (_hb_title->dovi.dv_profile && _hb_title->hdr_10_plus)
|
|
{
|
|
dynamicRange = [NSString stringWithFormat:@"Dolby Vision %d.%d HDR10+", _hb_title->dovi.dv_profile, _hb_title->dovi.dv_bl_signal_compatibility_id];
|
|
}
|
|
else if (_hb_title->dovi.dv_profile)
|
|
{
|
|
dynamicRange = [NSString stringWithFormat:@"Dolby Vision %d.%d", _hb_title->dovi.dv_profile, _hb_title->dovi.dv_bl_signal_compatibility_id];
|
|
}
|
|
|
|
[format appendFormat:@", %@ (", dynamicRange];
|
|
|
|
int bit_depth = hb_get_bit_depth(_hb_title->pix_fmt);
|
|
if (bit_depth)
|
|
{
|
|
[format appendFormat:@"%d-bit ", hb_get_bit_depth(_hb_title->pix_fmt)];
|
|
}
|
|
|
|
int h_shift, v_shift, chroma_available;
|
|
chroma_available = hb_get_chroma_sub_sample(_hb_title->pix_fmt, &h_shift, &v_shift);
|
|
if (chroma_available == 0)
|
|
{
|
|
int h_value = 4 >> h_shift;
|
|
int v_value = v_shift ? 0 : h_value;
|
|
[format appendFormat:@"4:%d:%d", h_value, v_value];
|
|
}
|
|
|
|
if (bit_depth || chroma_available == 0)
|
|
{
|
|
[format appendString:@", "];
|
|
}
|
|
|
|
[format appendFormat:@"%d-%d-%d)", _hb_title->color_prim, _hb_title->color_transfer, _hb_title->color_matrix];
|
|
|
|
hb_list_t *audioList = _hb_title->list_audio;
|
|
int audioCount = hb_list_count(audioList);
|
|
|
|
if (audioCount > 1)
|
|
{
|
|
[format appendFormat:HBKitLocalizedString(@", %d audio tracks", @"Title short description -> audio format"), audioCount];
|
|
}
|
|
else if (audioCount == 1)
|
|
{
|
|
[format appendString:HBKitLocalizedString(@", 1 audio track", @"Title short description -> audio format")];
|
|
}
|
|
|
|
hb_list_t *subList = _hb_title->list_subtitle;
|
|
int subCount = hb_list_count(subList);
|
|
|
|
if (subCount > 1)
|
|
{
|
|
[format appendFormat:HBKitLocalizedString(@", %d subtitles tracks", @"Title short description -> subtitles format"), subCount];
|
|
}
|
|
else if (subCount == 1)
|
|
{
|
|
[format appendString:HBKitLocalizedString(@", 1 subtitles track", @"Title short description -> subtitles format")];
|
|
}
|
|
|
|
return format;
|
|
}
|
|
|
|
- (NSURL *)url
|
|
{
|
|
return [NSURL fileURLWithPath:@(_hb_title->path)];
|
|
}
|
|
|
|
- (int)index
|
|
{
|
|
return self.hb_title->index;
|
|
}
|
|
|
|
- (BOOL)keepDuplicateTitles
|
|
{
|
|
return self.hb_title->keep_duplicate_titles;
|
|
}
|
|
|
|
- (int)angles
|
|
{
|
|
return self.hb_title->angle_count;
|
|
}
|
|
|
|
- (int)duration
|
|
{
|
|
return (self.hb_title->hours * 3600) + (self.hb_title->minutes * 60) + (self.hb_title->seconds);
|
|
}
|
|
|
|
- (int)frames
|
|
{
|
|
double frames = (double)self.hb_title->duration / 90000.f * self.hb_title->vrate.num / self.hb_title->vrate.den;
|
|
return (int)ceil(frames);
|
|
}
|
|
|
|
- (NSString *)timeCode
|
|
{
|
|
return [NSString stringWithFormat:@"%02d:%02d:%02d",
|
|
self.hb_title->hours, self.hb_title->minutes, self.hb_title->seconds];
|
|
}
|
|
|
|
- (int)width
|
|
{
|
|
return _hb_title->geometry.width;
|
|
}
|
|
|
|
- (int)height
|
|
{
|
|
return _hb_title->geometry.height;
|
|
}
|
|
|
|
- (int)parWidth
|
|
{
|
|
return _hb_title->geometry.par.num;
|
|
}
|
|
|
|
- (int)parHeight
|
|
{
|
|
return _hb_title->geometry.par.den;
|
|
}
|
|
|
|
- (int)autoCropTop
|
|
{
|
|
return _hb_title->crop[0];
|
|
}
|
|
|
|
- (int)autoCropBottom
|
|
{
|
|
return _hb_title->crop[1];
|
|
}
|
|
|
|
- (int)autoCropLeft
|
|
{
|
|
return _hb_title->crop[2];
|
|
}
|
|
|
|
- (int)autoCropRight
|
|
{
|
|
return _hb_title->crop[3];
|
|
}
|
|
|
|
- (int)looseAutoCropTop
|
|
{
|
|
return _hb_title->loose_crop[0];
|
|
}
|
|
|
|
- (int)looseAutoCropBottom
|
|
{
|
|
return _hb_title->loose_crop[1];
|
|
}
|
|
|
|
- (int)looseAutoCropLeft
|
|
{
|
|
return _hb_title->loose_crop[2];
|
|
}
|
|
|
|
- (int)looseAutoCropRight
|
|
{
|
|
return _hb_title->loose_crop[3];
|
|
}
|
|
|
|
- (NSArray<HBTitleAudioTrack *> *)audioTracks
|
|
{
|
|
if (!_audioTracks)
|
|
{
|
|
NSMutableArray<HBTitleAudioTrack *> *tracks = [NSMutableArray array];
|
|
hb_list_t *list = self.hb_title->list_audio;
|
|
int count = hb_list_count(list);
|
|
|
|
// Initialize the audio list of available audio tracks from this title
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
hb_audio_config_t *audio = hb_list_audio_config_item(list, i);
|
|
[tracks addObject:[[HBTitleAudioTrack alloc ] initWithAudioTrack:audio index:i]];
|
|
}
|
|
|
|
_audioTracks = [tracks copy];
|
|
}
|
|
|
|
return _audioTracks;
|
|
}
|
|
|
|
- (NSArray<HBTitleSubtitlesTrack *> *)subtitlesTracks
|
|
{
|
|
if (!_subtitlesTracks)
|
|
{
|
|
NSMutableArray<HBTitleSubtitlesTrack *> *tracks = [NSMutableArray array];
|
|
hb_list_t *list = self.hb_title->list_subtitle;
|
|
int count = hb_list_count(list);
|
|
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
hb_subtitle_t *subtitle = hb_list_item(self.hb_title->list_subtitle, i);
|
|
[tracks addObject:[[HBTitleSubtitlesTrack alloc] initWithSubtitlesTrack:subtitle index:i]];
|
|
}
|
|
|
|
_subtitlesTracks = [tracks copy];
|
|
}
|
|
|
|
return _subtitlesTracks;
|
|
}
|
|
|
|
- (NSArray<HBChapter *> *)chapters
|
|
{
|
|
if (!_chapters)
|
|
{
|
|
NSMutableArray<HBChapter *> *chapters = [NSMutableArray array];
|
|
uint64_t currentTime = 0;
|
|
for (int i = 0; i < hb_list_count(self.hb_title->list_chapter); i++)
|
|
{
|
|
hb_chapter_t *chapter = hb_list_item(self.hb_title->list_chapter, i);
|
|
|
|
if (chapter != NULL)
|
|
{
|
|
NSString *title;
|
|
if (chapter->title != NULL)
|
|
{
|
|
title = @(chapter->title);
|
|
}
|
|
|
|
if (title == NULL)
|
|
{
|
|
title = [NSString stringWithFormat:HBKitLocalizedString(@"Chapter %d", "Title -> chapter name"), i + 1];
|
|
}
|
|
|
|
[chapters addObject:[[HBChapter alloc] initWithTitle:title
|
|
index:i + 1
|
|
timestamp:currentTime
|
|
duration:chapter->duration]];
|
|
currentTime += chapter->duration;
|
|
}
|
|
}
|
|
|
|
_chapters = [chapters copy];
|
|
}
|
|
|
|
return _chapters;
|
|
}
|
|
|
|
- (nullable NSDictionary *)jobSettingsWithPreset:(HBPreset *)preset
|
|
{
|
|
NSDictionary *result = nil;
|
|
|
|
hb_dict_t *hb_preset = preset.content.hb_value;
|
|
hb_dict_t *job = hb_preset_job_init(self.hb_handle, self.hb_title->index, hb_preset);
|
|
|
|
if (job)
|
|
{
|
|
result = [[NSDictionary alloc] initWithHBDict:job];
|
|
}
|
|
|
|
hb_dict_free(&hb_preset);
|
|
hb_dict_free(&job);
|
|
|
|
return result;
|
|
}
|
|
|
|
@end
|