Skip to content

Integration with EXIFKit - #7

Merged
RuiNelson merged 16 commits into
mainfrom
next
Oct 24, 2026
Merged

RuiNelson merged 16 commits into
mainfrom
next

Conversation

@RuiNelson

Copy link
Copy Markdown
Owner

No description provided.

RuiNelson and others added 16 commits October 22, 2026 18:31
Add a SwiftSMBAVFoundation product that streams media files from an SMB
share into AVFoundation. SMB.Connection gains makeAsset(at:contentType:options:)
and makePlayerItem(at:contentType:), backed by an AVAssetResourceLoaderDelegate
that serves AVFoundation's byte-range requests from one shared read-only
handle. The content type is inferred from the path extension, falling back
to .movie. The module builds empty on watchOS, Linux, and Android.

Expose SMB.File.read(upTo:atOffset:transferChunkSize:), a positional read
over smb2_pread that leaves the handle's current offset untouched, so
concurrent tasks can read different ranges of one handle.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Build the "leave unchanged" basic-info timestamp sentinel with
truncatingIfNeeded so it compiles where time_t is 32-bit and still
matches libsmb2's 0xffffffff comparison in both widths.

Stop conversions that trap where Int is 32-bit: asUInt32's
Int(UInt32.max) bound, and server-supplied notify entry lengths,
maximum read/write sizes, and copy-chunk counts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The asset's resource loader keeps a read handle open until AVFoundation
releases it, and the server refuses to delete an open file with
STATUS_SHARING_VIOLATION. The reader and player item tests therefore left
their movie_*.mp4 behind on the test share. Retry the removal for up to
three seconds instead of ignoring the failure.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
SampleApp/SMB+AV connects to a share and plays an audio or video file with
AVPlayer through SwiftSMBAVFoundation's makePlayerItem(at:). It links the
local package, enables outgoing network connections in the macOS sandbox,
and adds the iOS local network usage description.

Document the sample app in the video playback guide, note that the asset
keeps the file open on the server while it is alive, and ignore the sample
project's generated xcworkspace.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Report the POSIX error code that matches an SMB.Error. POSIX errors keep
their own code, and SMB status errors use the code libsmb2's
nterror_to_errno maps their status to, such as ENOENT for
STATUS_OBJECT_NAME_NOT_FOUND and EACCES for STATUS_ACCESS_DENIED. A
closed connection, an invalid argument, and a failed context creation
report ENOTCONN, EINVAL, and ENOMEM.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
UTType(filenameExtension:) returns a dynamic type, not nil, for an
extension it doesn't know, so makeAsset(at:) reported types such as
dyn.ah62d4rv4ge81a2pwsu for .part or .bak files instead of the
documented .movie fallback. Ignore dynamic types when inferring the
content type.

Make the asset's loader and its content type internal, so the
integration tests can check the inferred type.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
AVFoundation keeps only the code of the error a loading request finishes
with and reads it as a POSIX errno. An SMB.Error bridges with its enum
case index as its code, so a missing file surfaced as ESRCH ("No such
process") and a closed connection as E2BIG ("Argument list too long").

Finish failed requests with an NSPOSIXErrorDomain error built from
SMB.Error.posixCode instead. A missing file now reports ENOENT, which
AVFoundation turns into NSURLErrorFileDoesNotExist, and a closed
connection ENOTCONN.

Correct the video playback guide, which promised the SMB error in the
player item's error, and show how to read the POSIX code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
AVFoundation never cancels the loading requests of an asset it releases,
not even after replaceCurrentItem(with: nil). Each serving task retained
the resource loader until it had read its whole range, which for AVPlayer
is often the rest of the file, so closing a player kept downloading the
movie in the background, with the file open on the server and the
connection busy. The loader's deinit, meant to cancel those tasks, could
never run while one was serving.

Serve requests from a RequestServer value that the tasks capture instead
of the loader, so releasing the asset releases the loader and its deinit
cancels the tasks.

The new integration test stalls the media connection behind a blocking
byte-range lock, seeks so that a request waits on it, releases the player
item, and checks that the loader goes away. TestMovie remuxes long copies
of its movie for it without re-encoding.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Open another connection to the same server and share, with the
credentials and configuration of the connection it is called on, so
callers can run operations in parallel without passing the credentials
around again. The connection keeps its credentials in a private
property for this; libsmb2 already keeps the password in the context
for the connection's lifetime.

The fork follows what is in effect on the original rather than what was
configured: its transferBlockSize is the larger of the accepted read and
write block sizes, so it transfers in the same blocks, and its timeout
is the current command timeout, including changes made with setTimeout.
The dialect and the other negotiation options are negotiated again from
the configured preferences. When no accepted block size can be
determined, the configured one is kept.

Document fork in the README, suggest it for playback connections in the
video playback guide, and correct the container name in AGENTS.md's
docker ps filter, which never matched the test server.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Write through smb2_pwrite, the counterpart of read(upTo:atOffset:): the
handle's current offset is neither used nor moved, so concurrent tasks
can write different ranges of one handle without coordinating seeks.
Writing past the end extends the file, the append option does not
apply, and a write that would end past UInt64.max throws EFBIG.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The new SwiftSMBEXIFKit product mirrors SwiftSFTPEXIFKit and re-exports
EXIFKit (from 0.3.0), which supports every platform SwiftSMB does.

- Read metadata with EXIFKit.read(file:), SMB.File.imageMetadata() and
  SMB.Connection.imageMetadata(at:). The connection variant reads the
  thumbnail with loadingThumbnail() and closes the file before returning.
- Edit and remove metadata with EXIFKit.write/strip, writeImageMetadata
  and removeImageMetadata on SMB.File and SMB.Connection. The connection
  variants write a temporary file next to the destination and rename it
  over it. SMB renames never replace a file, so an existing destination
  moves to a backup first, which is restored if the rename fails.
- The internal SMBFileHandleForEXIFKit adapts an SMB.File to EXIFKit's
  AbstractFileHandle with positional reads and writes, so EXIFKit's
  concurrent reads share one handle without a serial queue.
- Integration and cookbook tests, a guide in docs/image-metadata.md, and
  a README example that shows a thumbnail in a UIImage.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The positional write added in d7fa89a throws EFBIG through
POSIXErrorCode, which Foundation does not provide on Android.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@RuiNelson
RuiNelson merged commit 36d97c5 into main Oct 24, 2026
22 checks passed
@RuiNelson
RuiNelson deleted the next branch October 24, 2026 22:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant