Fixed leaky memory in PhotoKit, issue #276

This commit is contained in:
Rhet Turnbull
2021-01-09 17:24:06 -08:00
parent 248fdbcf02
commit db1947dd1e
4 changed files with 377 additions and 320 deletions

View File

@@ -1,5 +1,5 @@
""" version info """ """ version info """
__version__ = "0.39.12" __version__ = "0.39.13"

View File

@@ -19,6 +19,7 @@
# add original=False to export instead of version= (and maybe others like path()) # add original=False to export instead of version= (and maybe others like path())
# make burst/live methods get uuid from self instead of passing as arg # make burst/live methods get uuid from self instead of passing as arg
import copy
import pathlib import pathlib
import threading import threading
import time import time
@@ -169,12 +170,14 @@ class ImageData:
requestImageDataAndOrientationForAsset_options_resultHandler_ requestImageDataAndOrientationForAsset_options_resultHandler_
""" """
def __init__(self): def __init__(
self.metadata = None self, metadata=None, uti=None, image_data=None, info=None, orientation=None
self.uti = None ):
self.image_data = None self.metadata = metadata
self.info = None self.uti = uti
self.orientation = None self.image_data = image_data
self.info = info
self.orientation = orientation
class AVAssetData: class AVAssetData:
@@ -475,8 +478,11 @@ class PhotoAsset:
# if self.live: # if self.live:
# raise NotImplementedError("Live photos not implemented yet") # raise NotImplementedError("Live photos not implemented yet")
with objc.autorelease_pool():
filename = ( filename = (
pathlib.Path(filename) if filename else pathlib.Path(self.original_filename) pathlib.Path(filename)
if filename
else pathlib.Path(self.original_filename)
) )
dest = pathlib.Path(dest) dest = pathlib.Path(dest)
@@ -495,6 +501,7 @@ class PhotoAsset:
with open(output_file, "wb") as fd: with open(output_file, "wb") as fd:
fd.write(imagedata.image_data) fd.write(imagedata.image_data)
del imagedata
elif self.ismovie: elif self.ismovie:
videodata = self._request_video_data(version=version) videodata = self._request_video_data(version=version)
if videodata.asset is None: if videodata.asset is None:
@@ -529,6 +536,7 @@ class PhotoAsset:
# reference: https://developer.apple.com/documentation/photokit/phimagemanager/3237282-requestimagedataandorientationfo?language=objc # reference: https://developer.apple.com/documentation/photokit/phimagemanager/3237282-requestimagedataandorientationfo?language=objc
with objc.autorelease_pool():
if version not in [ if version not in [
PHOTOS_VERSION_CURRENT, PHOTOS_VERSION_CURRENT,
PHOTOS_VERSION_ORIGINAL, PHOTOS_VERSION_ORIGINAL,
@@ -571,8 +579,13 @@ class PhotoAsset:
self.phasset, options_request, handler self.phasset, options_request, handler
) )
event.wait() event.wait()
self._imagedata = requestdata # options_request.dealloc()
return requestdata
# not sure why this is needed -- some weird ref count thing maybe
# if I don't do this, memory leaks
data = copy.copy(requestdata)
del requestdata
return data
def _make_result_handle_(self, data): def _make_result_handle_(self, data):
""" Make handler function and threading event to use with """ Make handler function and threading event to use with
@@ -634,6 +647,8 @@ class SlowMoVideoExporter(NSObject):
Returns: Returns:
path to exported file path to exported file
""" """
with objc.autorelease_pool():
exporter = AVFoundation.AVAssetExportSession.alloc().initWithAsset_presetName_( exporter = AVFoundation.AVAssetExportSession.alloc().initWithAsset_presetName_(
self.avasset, AVFoundation.AVAssetExportPresetHighestQuality self.avasset, AVFoundation.AVAssetExportPresetHighestQuality
) )
@@ -664,7 +679,9 @@ class SlowMoVideoExporter(NSObject):
) )
time.sleep(MIN_SLEEP) time.sleep(MIN_SLEEP)
return NSURL_to_path(exporter.outputURL()) exported_path = NSURL_to_path(exporter.outputURL())
# exporter.dealloc()
return exported_path
def __del__(self): def __del__(self):
self.avasset = None self.avasset = None
@@ -701,6 +718,7 @@ class VideoAsset(PhotoAsset):
ValueError if dest is not a valid directory ValueError if dest is not a valid directory
""" """
with objc.autorelease_pool():
if self.slow_mo and version == PHOTOS_VERSION_CURRENT: if self.slow_mo and version == PHOTOS_VERSION_CURRENT:
return [ return [
self._export_slow_mo( self._export_slow_mo(
@@ -709,7 +727,9 @@ class VideoAsset(PhotoAsset):
] ]
filename = ( filename = (
pathlib.Path(filename) if filename else pathlib.Path(self.original_filename) pathlib.Path(filename)
if filename
else pathlib.Path(self.original_filename)
) )
dest = pathlib.Path(dest) dest = pathlib.Path(dest)
@@ -723,6 +743,7 @@ class VideoAsset(PhotoAsset):
url = videodata.asset.URL() url = videodata.asset.URL()
path = pathlib.Path(NSURL_to_path(url)) path = pathlib.Path(NSURL_to_path(url))
del videodata
if not path.is_file(): if not path.is_file():
raise FileNotFoundError("Could not get path to video file") raise FileNotFoundError("Could not get path to video file")
ext = path.suffix ext = path.suffix
@@ -752,6 +773,7 @@ class VideoAsset(PhotoAsset):
Raises: Raises:
ValueError if dest is not a valid directory ValueError if dest is not a valid directory
""" """
with objc.autorelease_pool():
if not self.slow_mo: if not self.slow_mo:
raise PhotoKitMediaTypeError("Not a slow-mo video") raise PhotoKitMediaTypeError("Not a slow-mo video")
@@ -763,7 +785,9 @@ class VideoAsset(PhotoAsset):
raise PhotoKitMediaTypeError("Does not appear to be slow-mo video") raise PhotoKitMediaTypeError("Does not appear to be slow-mo video")
filename = ( filename = (
pathlib.Path(filename) if filename else pathlib.Path(self.original_filename) pathlib.Path(filename)
if filename
else pathlib.Path(self.original_filename)
) )
dest = pathlib.Path(dest) dest = pathlib.Path(dest)
@@ -778,7 +802,9 @@ class VideoAsset(PhotoAsset):
exporter = SlowMoVideoExporter.alloc().initWithAVAsset_path_( exporter = SlowMoVideoExporter.alloc().initWithAVAsset_path_(
videodata.asset, output_file videodata.asset, output_file
) )
return exporter.exportSlowMoVideo() video = exporter.exportSlowMoVideo()
# exporter.dealloc()
return video
# todo: rewrite this with NotificationCenter and App event loop? # todo: rewrite this with NotificationCenter and App event loop?
def _request_video_data(self, version=PHOTOS_VERSION_ORIGINAL): def _request_video_data(self, version=PHOTOS_VERSION_ORIGINAL):
@@ -793,7 +819,7 @@ class VideoAsset(PhotoAsset):
Raises: Raises:
ValueError if passed invalid value for version ValueError if passed invalid value for version
""" """
with objc.autorelease_pool():
if version not in [ if version not in [
PHOTOS_VERSION_CURRENT, PHOTOS_VERSION_CURRENT,
PHOTOS_VERSION_ORIGINAL, PHOTOS_VERSION_ORIGINAL,
@@ -824,7 +850,12 @@ class VideoAsset(PhotoAsset):
self.phasset, options_request, handler self.phasset, options_request, handler
) )
event.wait() event.wait()
return requestdata
# not sure why this is needed -- some weird ref count thing maybe
# if I don't do this, memory leaks
data = copy.copy(requestdata)
del requestdata
return data
class LivePhotoRequest(NSObject): class LivePhotoRequest(NSObject):
@@ -843,6 +874,8 @@ class LivePhotoRequest(NSObject):
def requestLivePhotoResources(self, version=PHOTOS_VERSION_CURRENT): def requestLivePhotoResources(self, version=PHOTOS_VERSION_CURRENT):
""" return the photos and video components of a live video as [PHAssetResource] """ """ return the photos and video components of a live video as [PHAssetResource] """
with objc.autorelease_pool():
options = Photos.PHLivePhotoRequestOptions.alloc().init() options = Photos.PHLivePhotoRequestOptions.alloc().init()
options.setNetworkAccessAllowed_(True) options.setNetworkAccessAllowed_(True)
options.setVersion_(version) options.setVersion_(version)
@@ -883,7 +916,12 @@ class LivePhotoRequest(NSObject):
asset_resources = Photos.PHAssetResource.assetResourcesForLivePhoto_( asset_resources = Photos.PHAssetResource.assetResourcesForLivePhoto_(
self.live_photo self.live_photo
) )
return asset_resources
# not sure why this is needed -- some weird ref count thing maybe
# if I don't do this, memory leaks
data = copy.copy(asset_resources)
del asset_resources
return data
def __del__(self): def __del__(self):
self.manager = None self.manager = None
@@ -923,8 +961,12 @@ class LivePhotoAsset(PhotoAsset):
ValueError if dest is not a valid directory ValueError if dest is not a valid directory
PhotoKitExportError if error during export PhotoKitExportError if error during export
""" """
with objc.autorelease_pool():
filename = ( filename = (
pathlib.Path(filename) if filename else pathlib.Path(self.original_filename) pathlib.Path(filename)
if filename
else pathlib.Path(self.original_filename)
) )
dest = pathlib.Path(dest) dest = pathlib.Path(dest)
@@ -949,9 +991,13 @@ class LivePhotoAsset(PhotoAsset):
"Did not find photo/video resources for live photo" "Did not find photo/video resources for live photo"
) )
photo_ext = get_preferred_uti_extension(photo_resource.uniformTypeIdentifier()) photo_ext = get_preferred_uti_extension(
photo_resource.uniformTypeIdentifier()
)
photo_output_file = dest / f"{filename.stem}.{photo_ext}" photo_output_file = dest / f"{filename.stem}.{photo_ext}"
video_ext = get_preferred_uti_extension(video_resource.uniformTypeIdentifier()) video_ext = get_preferred_uti_extension(
video_resource.uniformTypeIdentifier()
)
video_output_file = dest / f"{filename.stem}.{video_ext}" video_output_file = dest / f"{filename.stem}.{video_ext}"
if not overwrite: if not overwrite:
@@ -998,12 +1044,15 @@ class LivePhotoAsset(PhotoAsset):
with open(photo_output_file, "wb") as fd: with open(photo_output_file, "wb") as fd:
fd.write(data) fd.write(data)
exported.append(str(photo_output_file)) exported.append(str(photo_output_file))
del data
if video: if video:
data = self._request_resource_data(video_resource) data = self._request_resource_data(video_resource)
with open(video_output_file, "wb") as fd: with open(video_output_file, "wb") as fd:
fd.write(data) fd.write(data)
exported.append(str(video_output_file)) exported.append(str(video_output_file))
del data
request.dealloc()
return exported return exported
def _request_resource_data(self, resource): def _request_resource_data(self, resource):
@@ -1015,6 +1064,7 @@ class LivePhotoAsset(PhotoAsset):
Raises: Raises:
""" """
with objc.autorelease_pool():
resource_manager = Photos.PHAssetResourceManager.defaultManager() resource_manager = Photos.PHAssetResourceManager.defaultManager()
options = Photos.PHAssetResourceRequestOptions.alloc().init() options = Photos.PHAssetResourceRequestOptions.alloc().init()
options.setNetworkAccessAllowed_(True) options.setNetworkAccessAllowed_(True)
@@ -1032,7 +1082,9 @@ class LivePhotoAsset(PhotoAsset):
def completion_handler(error): def completion_handler(error):
if error: if error:
raise PhotoKitExportError("Error requesting data for asset resource") raise PhotoKitExportError(
"Error requesting data for asset resource"
)
event.set() event.set()
resource_manager.requestDataForAssetResource_options_dataReceivedHandler_completionHandler_( resource_manager.requestDataForAssetResource_options_dataReceivedHandler_completionHandler_(
@@ -1040,8 +1092,12 @@ class LivePhotoAsset(PhotoAsset):
) )
event.wait() event.wait()
options.dealloc()
return requestdata.data # not sure why this is needed -- some weird ref count thing maybe
# if I don't do this, memory leaks
data = copy.copy(requestdata.data)
del requestdata
return data
# def request_image_data(self, version=PHOTOS_VERSION_CURRENT): # def request_image_data(self, version=PHOTOS_VERSION_CURRENT):
# # Returns an NSImage which isn't overly useful # # Returns an NSImage which isn't overly useful
@@ -1127,6 +1183,7 @@ class PhotoLibrary:
""" """
# pylint: disable=no-member # pylint: disable=no-member
with objc.autorelease_pool():
fetch_options = Photos.PHFetchOptions.alloc().init() fetch_options = Photos.PHFetchOptions.alloc().init()
fetch_result = Photos.PHAsset.fetchAssetsWithLocalIdentifiers_options_( fetch_result = Photos.PHAsset.fetchAssetsWithLocalIdentifiers_options_(
uuid_list, fetch_options uuid_list, fetch_options

View File

@@ -260,7 +260,7 @@ def get_preferred_uti_extension(uti):
returns: preferred extension as str """ returns: preferred extension as str """
# reference: https://developer.apple.com/documentation/coreservices/1442744-uttypecopypreferredtagwithclass?language=objc # reference: https://developer.apple.com/documentation/coreservices/1442744-uttypecopypreferredtagwithclass?language=objc
with objc.autorelease_pool():
return CoreServices.UTTypeCopyPreferredTagWithClass( return CoreServices.UTTypeCopyPreferredTagWithClass(
uti, CoreServices.kUTTagClassFilenameExtension uti, CoreServices.kUTTagClassFilenameExtension
) )

File diff suppressed because one or more lines are too long