/* HBCore.m $
This file is part of the HandBrake source code.
Homepage: .
It may be used under the terms of the GNU General Public License. */
#import "HBCore.h"
#import "HBJob.h"
#import "HBJob+HBJobConversion.h"
#import "HBDVDDetector.h"
#import "HBUtilities.h"
#import "HBStateFormatter+Private.h"
#import "HBTitle+Private.h"
#include
static BOOL globalInitialized = NO;
static void (^errorHandler)(NSString *error) = NULL;
static void hb_error_handler(const char *errmsg)
{
NSString *error = @(errmsg);
if (error)
{
errorHandler(error);
}
}
typedef void (^HBCoreCleanupHandler)();
/**
* Private methods of HBCore.
*/
@interface HBCore ()
/// Pointer to a hb_state_s struct containing the detailed state information of libhb.
@property (nonatomic, readonly) hb_state_t *hb_state;
/// Pointer to a libhb handle used by this HBCore instance.
@property (nonatomic, readonly) hb_handle_t *hb_handle;
/// Current state of HBCore.
@property (nonatomic, readwrite) HBState state;
/// Timer used to poll libhb for state changes.
@property (nonatomic, readwrite) dispatch_source_t updateTimer;
@property (nonatomic, readonly) dispatch_queue_t updateTimerQueue;
/// Current scanned titles.
@property (nonatomic, readwrite, copy) NSArray *titles;
/// Progress handler.
@property (nonatomic, readwrite, copy) HBCoreProgressHandler progressHandler;
/// Completion handler.
@property (nonatomic, readwrite, copy) HBCoreCompletionHandler completionHandler;
/// Cleanup handle, used for internal HBCore cleanup.
@property (nonatomic, readwrite, copy) HBCoreCleanupHandler cleanupHandler;
@end
@implementation HBCore
+ (void)setDVDNav:(BOOL)enabled
{
hb_dvd_set_dvdnav(enabled);
}
+ (void)initGlobal
{
hb_global_init();
globalInitialized = YES;
}
+ (void)closeGlobal
{
NSAssert(globalInitialized, @"[HBCore closeGlobal] global closed but not initialized");
hb_global_close();
}
+ (void)registerErrorHandler:(void (^)(NSString *error))handler
{
errorHandler = [handler copy];
hb_register_error_handler(&hb_error_handler);
}
- (instancetype)init
{
return [self initWithLogLevel:0 queue:dispatch_get_main_queue()];
}
- (instancetype)initWithLogLevel:(int)level queue:(dispatch_queue_t)queue
{
self = [super init];
if (self)
{
_name = @"HBCore";
_automaticallyPreventSleep = YES;
_state = HBStateIdle;
_updateTimerQueue = queue;
_titles = @[];
_stateFormatter = [[HBStateFormatter alloc] init];
_hb_state = malloc(sizeof(hb_state_t));
bzero(_hb_state, sizeof(hb_state_t));
_logLevel = level;
_hb_handle = hb_init(level);
if (!_hb_handle)
{
return nil;
}
}
return self;
}
- (instancetype)initWithLogLevel:(int)level name:(NSString *)name
{
self = [self initWithLogLevel:level queue:dispatch_get_main_queue()];
if (self)
{
_name = [name copy];
}
return self;
}
/**
* Releases resources.
*/
- (void)dealloc
{
[self stopUpdateTimer];
hb_close(&_hb_handle);
_hb_handle = NULL;
free(_hb_state);
}
- (void)setLogLevel:(int)logLevel
{
_logLevel = logLevel;
hb_log_level_set(_hb_handle, logLevel);
}
- (void)preventSleep
{
NSAssert(!self.automaticallyPreventSleep, @"[HBCore preventSleep:] called with automaticallyPreventSleep enabled.");
hb_system_sleep_prevent(_hb_handle);
}
- (void)allowSleep
{
NSAssert(!self.automaticallyPreventSleep, @"[HBCore allowSleep:] called with automaticallyPreventSleep enabled.");
hb_system_sleep_allow(_hb_handle);
}
- (void)preventAutoSleep
{
if (self.automaticallyPreventSleep)
{
hb_system_sleep_prevent(_hb_handle);
}
}
- (void)allowAutoSleep
{
if (self.automaticallyPreventSleep)
{
hb_system_sleep_allow(_hb_handle);
}
}
#pragma mark - Scan
- (BOOL)canScan:(NSURL *)url error:(NSError * __autoreleasing *)error
{
NSAssert(url, @"[HBCore canScan:] called with nil url.");
#ifdef __SANDBOX_ENABLED__
BOOL accessingSecurityScopedResource = [url startAccessingSecurityScopedResource];
#endif
if (![[NSFileManager defaultManager] fileExistsAtPath:url.path]) {
if (error) {
*error = [NSError errorWithDomain:@"HBErrorDomain"
code:100
userInfo:@{ NSLocalizedDescriptionKey: @"Unable to find the file at the specified URL" }];
}
return NO;
}
HBDVDDetector *detector = [HBDVDDetector detectorForPath:url.path];
if (detector.isVideoDVD || detector.isVideoBluRay)
{
[HBUtilities writeToActivityLog:"%s trying to open a physical disc at: %s", self.name.UTF8String, url.path.UTF8String];
void *lib = NULL;
if (detector.isVideoDVD)
{
lib = dlopen("libdvdcss.2.dylib", RTLD_LAZY);
}
else if (detector.isVideoBluRay)
{
lib = dlopen("libaacs.dylib", RTLD_LAZY);
}
if (lib)
{
dlclose(lib);
[HBUtilities writeToActivityLog:"%s library found for decrypting physical disc", self.name.UTF8String];
}
else
{
// Notify the user that we don't support removal of copy protection.
[HBUtilities writeToActivityLog:"%s, library not found for decrypting physical disc", self.name.UTF8String];
if (error) {
*error = [NSError errorWithDomain:@"HBErrorDomain"
code:101
userInfo:@{ NSLocalizedDescriptionKey: @"library not found for decrypting physical disc" }];
}
}
}
#ifdef __SANDBOX_ENABLED__
if (accessingSecurityScopedResource)
{
[url stopAccessingSecurityScopedResource];
}
#endif
return YES;
}
- (void)scanURL:(NSURL *)url titleIndex:(NSUInteger)index previews:(NSUInteger)previewsNum minDuration:(NSUInteger)seconds progressHandler:(HBCoreProgressHandler)progressHandler completionHandler:(HBCoreCompletionHandler)completionHandler
{
NSAssert(self.state == HBStateIdle, @"[HBCore scanURL:] called while another scan or encode already in progress");
NSAssert(url, @"[HBCore scanURL:] called with nil url.");
#ifdef __SANDBOX_ENABLED__
BOOL accessingSecurityScopedResource = [url startAccessingSecurityScopedResource];
self.cleanupHandler = ^{
if (accessingSecurityScopedResource)
{
[url stopAccessingSecurityScopedResource];
}
};
#endif
// Reset the titles array
self.titles = @[];
// Copy the progress/completion blocks
self.progressHandler = progressHandler;
self.completionHandler = completionHandler;
// convert minTitleDuration from seconds to the internal HB time
uint64_t min_title_duration_ticks = 90000LL * seconds;
// If there is no title number passed to scan, we use 0
// which causes the default behavior of a full source scan
if (index > 0)
{
[HBUtilities writeToActivityLog:"%s scanning specifically for title: %d", self.name.UTF8String, index];
}
else
{
// minimum title duration doesn't apply to title-specific scan
// it doesn't apply to batch scan either, but we can't tell it apart from DVD & BD folders here
[HBUtilities writeToActivityLog:"%s scanning titles with a duration of %d seconds or more", self.name.UTF8String, seconds];
}
[self preventAutoSleep];
hb_scan(_hb_handle, url.path.fileSystemRepresentation,
(int)index, (int)previewsNum,
1, min_title_duration_ticks);
// Start the timer to handle libhb state changes
[self startUpdateTimerWithInterval:0.2];
// Set the state, so the UI can be update
// to reflect the current state instead of
// waiting for libhb to set it in a background thread.
self.state = HBStateScanning;
}
/**
* Creates an array of lightweight HBTitles instances.
*/
- (HBCoreResult)scanDone
{
hb_title_set_t *title_set = hb_get_title_set(_hb_handle);
NSMutableArray *titles = [NSMutableArray array];
for (int i = 0; i < hb_list_count(title_set->list_title); i++)
{
hb_title_t *title = (hb_title_t *) hb_list_item(title_set->list_title, i);
[titles addObject:[[HBTitle alloc] initWithTitle:title handle:self.hb_handle featured:(title->index == title_set->feature)]];
}
self.titles = [titles copy];
[HBUtilities writeToActivityLog:"%s scan done", self.name.UTF8String];
return (self.titles.count > 0) ? HBCoreResultDone : HBCoreResultFailed;
}
- (void)cancelScan
{
hb_scan_stop(_hb_handle);
[HBUtilities writeToActivityLog:"%s scan cancelled", self.name.UTF8String];
}
#pragma mark - Preview images
- (CGImageRef)CGImageRotatedByAngle:(CGImageRef)imgRef angle:(CGFloat)angle flipped:(BOOL)flipped CF_RETURNS_RETAINED
{
CGFloat angleInRadians = angle * (M_PI / 180);
CGFloat width = CGImageGetWidth(imgRef);
CGFloat height = CGImageGetHeight(imgRef);
CGRect imgRect = CGRectMake(0, 0, width, height);
CGAffineTransform transform = CGAffineTransformMakeRotation(angleInRadians);
CGRect rotatedRect = CGRectApplyAffineTransform(imgRect, transform);
CGColorSpaceRef colorSpace = CGImageGetColorSpace(imgRef);
CGContextRef bmContext = CGBitmapContextCreate(NULL,
(size_t)rotatedRect.size.width,
(size_t)rotatedRect.size.height,
8,
0,
colorSpace,
kCGImageAlphaPremultipliedFirst);
CGContextSetAllowsAntialiasing(bmContext, FALSE);
CGContextSetInterpolationQuality(bmContext, kCGInterpolationNone);
// Rotate
CGContextTranslateCTM(bmContext,
+ (rotatedRect.size.width / 2),
+ (rotatedRect.size.height / 2));
CGContextRotateCTM(bmContext, -angleInRadians);
CGContextTranslateCTM(bmContext,
- (rotatedRect.size.width / 2),
- (rotatedRect.size.height / 2));
// Flip
if (flipped)
{
CGAffineTransform flipHorizontal = CGAffineTransformMake(-1, 0, 0, 1, floor(rotatedRect.size.width), 0);
CGContextConcatCTM(bmContext, flipHorizontal);
}
CGContextDrawImage(bmContext,
CGRectMake((rotatedRect.size.width - width)/2.0f,
(rotatedRect.size.height - height)/2.0f,
width,
height),
imgRef);
CGImageRef rotatedImage = CGBitmapContextCreateImage(bmContext);
CFRelease(bmContext);
return rotatedImage;
}
- (CGColorSpaceRef)copyColorSpaceWithColorPrimaries:(int)colorPrimaries
{
const CGFloat whitePoint[] = {0.95047, 1.0, 1.08883};
const CGFloat blackPoint[] = {0, 0, 0};
// See https://developer.apple.com/library/content/technotes/tn2257/_index.html
const CGFloat gamma[] = {1.961, 1.961, 1.961};
// RGB/XYZ Matrices (D65 white point)
switch (colorPrimaries) {
case HB_COLR_PRI_EBUTECH:
{
// Rec. 601, 625 line
const CGFloat matrix[] = {0.4305538, 0.2220043, 0.0201822,
0.3415498, 0.7066548, 0.1295534,
0.1783523, 0.0713409, 0.9393222};
return CGColorSpaceCreateCalibratedRGB(whitePoint, blackPoint, gamma, matrix);
}
case HB_COLR_PRI_SMPTEC:
{
// Rec. 601, 525 line
const CGFloat matrix[] = {0.3935209, 0.2123764, 0.0187391,
0.3652581, 0.7010599, 0.1119339,
0.1916769, 0.0865638, 0.9583847};
return CGColorSpaceCreateCalibratedRGB(whitePoint, blackPoint, gamma, matrix);
}
case HB_COLR_PRI_BT2020:
{
// Rec. 2020
const CGFloat matrix[] = {0.6369580, 0.2627002, 0.0000000,
0.1446169, 0.6779981, 0.0280727,
0.1688810, 0.0593017, 1.0609851};
return CGColorSpaceCreateCalibratedRGB(whitePoint, blackPoint, gamma, matrix);
}
case HB_COLR_PRI_BT709:
default:
{
// Rec. 709
const CGFloat matrix[] = {0.4124564, 0.2126729, 0.0193339,
0.3575761, 0.7151522, 0.1191920,
0.1804375, 0.0721750, 0.9503041};
return CGColorSpaceCreateCalibratedRGB(whitePoint, blackPoint, gamma, matrix);
}
}
}
- (CGImageRef)copyImageAtIndex:(NSUInteger)index
forTitle:(HBTitle *)title
pictureFrame:(HBPicture *)frame
deinterlace:(BOOL)deinterlace
rotate:(int)angle
flipped:(BOOL)flipped CF_RETURNS_RETAINED
{
CGImageRef img = NULL;
hb_geometry_settings_t geo;
memset(&geo, 0, sizeof(geo));
geo.geometry.width = frame.displayWidth;
geo.geometry.height = frame.height;
// ignore the par.
geo.geometry.par.num = 1;
geo.geometry.par.den = 1;
int crop[4] = {frame.cropTop, frame.cropBottom, frame.cropLeft, frame.cropRight};
memcpy(geo.crop, crop, sizeof(int[4]));
hb_image_t *image = hb_get_preview2(_hb_handle, title.index, (int)index, &geo, deinterlace);
if (image)
{
// Create an CGImageRef and copy the libhb image into it.
// The image data returned by hb_get_preview2 is 4 bytes per pixel, BGRA format.
// Alpha is ignored.
CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNone;
CFMutableDataRef imgData = CFDataCreateMutable(kCFAllocatorDefault, 3 * image->width * image->height);
CGDataProviderRef provider = CGDataProviderCreateWithCFData(imgData);
CGColorSpaceRef colorSpace = [self copyColorSpaceWithColorPrimaries:title.hb_title->color_prim];
img = CGImageCreate(image->width,
image->height,
8,
24,
image->width * 3,
colorSpace,
bitmapInfo,
provider,
NULL,
NO,
kCGRenderingIntentDefault);
CGColorSpaceRelease(colorSpace);
CGDataProviderRelease(provider);
CFRelease(imgData);
UInt8 *src_line = image->data;
UInt8 *dst = CFDataGetMutableBytePtr(imgData);
for (int r = 0; r < image->height; r++)
{
UInt8 *src = src_line;
for (int c = 0; c < image->width; c++)
{
*dst++ = src[2];
*dst++ = src[1];
*dst++ = src[0];
src += 4;
}
src_line += image->plane[0].stride;
}
hb_image_close(&image);
}
if (angle || flipped)
{
CGImageRef rotatedImg = [self CGImageRotatedByAngle:img angle:angle flipped:flipped];
CGImageRelease(img);
return rotatedImg;
}
else
{
return img;
}
}
- (NSUInteger)imagesCountForTitle:(HBTitle *)title
{
return title.hb_title->preview_count;
}
#pragma mark - Encodes
- (void)encodeJob:(HBJob *)job progressHandler:(HBCoreProgressHandler)progressHandler completionHandler:(HBCoreCompletionHandler)completionHandler;
{
NSAssert(self.state == HBStateIdle, @"[HBCore encodeJob:] called while another scan or encode already in progress");
NSAssert(job, @"[HBCore encodeJob:] called with nil job");
// Copy the progress/completion blocks
self.progressHandler = progressHandler;
self.completionHandler = completionHandler;
#ifdef __SANDBOX_ENABLED__
HBJob *jobCopy = [job copy];
[jobCopy startAccessingSecurityScopedResource];
self.cleanupHandler = ^{ [jobCopy stopAccessingSecurityScopedResource]; };
#endif
// Add the job to libhb
hb_job_t *hb_job = job.hb_job;
hb_job_set_file(hb_job, job.completeOutputURL.path.fileSystemRepresentation);
hb_add(_hb_handle, hb_job);
// Free the job
hb_job_close(&hb_job);
[self preventAutoSleep];
hb_start(_hb_handle);
// Start the timer to handle libhb state changes
[self startUpdateTimerWithInterval:0.5];
// Set the state, so the UI can be update
// to reflect the current state instead of
// waiting for libhb to set it in a background thread.
self.state = HBStateWorking;
[HBUtilities writeToActivityLog:"%s started encoding %s", self.name.UTF8String, job.outputFileName.UTF8String];
[HBUtilities writeToActivityLog:"%s with preset %s", self.name.UTF8String, job.presetName.UTF8String];
}
- (HBCoreResult)workDone
{
// HB_STATE_WORKDONE happpens as a result of libhb finishing all its jobs
// or someone calling hb_stop. In the latter case, hb_stop does not clear
// out the remaining passes/jobs in the queue. We'll do that here.
hb_job_t *job;
while ((job = hb_job(_hb_handle, 0)))
{
hb_rem(_hb_handle, job);
}
HBCoreResult result = HBCoreResultDone;
switch (_hb_state->param.workdone.error)
{
case HB_ERROR_NONE:
result = HBCoreResultDone;
[HBUtilities writeToActivityLog:"%s work done", self.name.UTF8String];
break;
case HB_ERROR_CANCELED:
result = HBCoreResultCancelled;
[HBUtilities writeToActivityLog:"%s work canceled", self.name.UTF8String];
break;
default:
result = HBCoreResultFailed;
[HBUtilities writeToActivityLog:"%s work failed", self.name.UTF8String];
break;
}
return result;
}
- (void)cancelEncode
{
hb_stop(_hb_handle);
[HBUtilities writeToActivityLog:"%s encode canceled", self.name.UTF8String];
}
- (void)pause
{
hb_pause(_hb_handle);
self.state = HBStatePaused;
[self allowAutoSleep];
}
- (void)resume
{
hb_resume(_hb_handle);
self.state = HBStateWorking;
[self preventAutoSleep];
}
#pragma mark - State updates
/**
* Starts the timer used to polls libhb for state changes.
*
* @param seconds The number of seconds between firings of the timer.
*/
- (void)startUpdateTimerWithInterval:(NSTimeInterval)seconds
{
if (!self.updateTimer)
{
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _updateTimerQueue);
if (timer)
{
dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), (uint64_t)(seconds * NSEC_PER_SEC), (uint64_t)(seconds * NSEC_PER_SEC / 10));
dispatch_source_set_event_handler(timer, ^{
[self updateState];
});
dispatch_resume(timer);
}
self.updateTimer = timer;
}
}
/**
* Stops the update timer.
*/
- (void)stopUpdateTimer
{
if (self.updateTimer)
{
dispatch_source_cancel(self.updateTimer);
dispatch_release(self.updateTimer);
self.updateTimer = NULL;
}
}
/**
* This method polls libhb continuously for state changes and processes them.
* Additional processing for each state is performed in methods that start
* with 'handle'.
*/
- (void)updateState
{
hb_get_state(_hb_handle, _hb_state);
if (_hb_state->state == HB_STATE_IDLE)
{
// Libhb reported HB_STATE_IDLE, so nothing interesting has happened.
return;
}
// Update HBCore state to reflect the current state of libhb
self.state = _hb_state->state;
// Call the handler for the current state
if (_hb_state->state == HB_STATE_WORKDONE || _hb_state->state == HB_STATE_SCANDONE)
{
[self handleCompletion];
}
else
{
[self handleProgress];
}
}
#pragma mark - Blocks callbacks
/**
* Processes progress state information.
*/
- (void)handleProgress
{
if (self.progressHandler)
{
hb_state_t state = *(self.hb_state);
HBProgress progress = {0, 0, 0, 0};
progress.percent = [self.stateFormatter stateToPercentComplete:state];
if (state.state == HB_STATE_WORKING || state.state == HB_STATE_PAUSED)
{
progress.hours = state.param.working.hours;
progress.minutes = state.param.working.minutes;
progress.seconds = state.param.working.seconds;
}
NSString *info = [self.stateFormatter stateToString:state];
self.progressHandler(self.state, progress, info);
}
}
/**
* Processes completion state information.
*/
- (void)handleCompletion
{
// Libhb reported HB_STATE_WORKDONE or HB_STATE_SCANDONE,
// so nothing interesting will happen after this point, stop the timer.
[self stopUpdateTimer];
// Set the state to idle, because the update timer won't fire again.
self.state = HBStateIdle;
// Reallow system sleep.
[self allowAutoSleep];
// Call the completion block and clean ups the handlers
self.progressHandler = nil;
#ifdef __SANDBOX_ENABLED__
self.cleanupHandler();
self.cleanupHandler = nil;
#endif
HBCoreResult result = (_hb_state->state == HB_STATE_WORKDONE) ? [self workDone] : [self scanDone];
[self runCompletionBlockAndCleanUpWithResult:result];
}
/**
* Runs the completion block and clean ups the internal blocks.
*
* @param result the result to pass to the completion block.
*/
- (void)runCompletionBlockAndCleanUpWithResult:(HBCoreResult)result
{
if (self.completionHandler)
{
// Retain the completion block, because it could be replaced
// inside the same block.
HBCoreCompletionHandler completionHandler = self.completionHandler;
self.completionHandler = nil;
completionHandler(result);
}
}
@end