Multipeer Connectivity - Get file transfer(Internet) speed and File Size in Swift 5
Asked Answered
C

1

7

I am transferring photo peer to peer. All things works fine but I am not able to get the photo(file) transfer speed i.g internet speed. Like MB the file is transferred. Second I want to fetch the size of that file.

We are passing photo in data format using MCSession

Due to privacy I cannot add the project code here but I will share the refrence github project that I followed. In project I am passing string and In my case its Photo. All things are same.

I checked in Stackoverflow but not found any accurate answer!

Reference Project Link: https://github.com/YogeshPateliOS/MultipeerConnectivity-.git

Thank You!

Chickpea answered 9/1, 2022 at 16:2 Comment(2)
I understand you do not want to share the whole code base, however, could you share some code that you are using to transfer the file from one device to another.Riff
Yes I added a demo link please check that!Chickpea
R
12

TLDR: If you do not want to read the long explanation and get straight to the code, all the ideas below are brought together and can be tested by downloading my public repository which has comments to explain all of this.

So here are my suggestions on how you can achieve this

After reviewing your code, I see that you are using the following function to send data

func send(_ data: Data, toPeers peerIDs: [MCPeerID], with mode: MCSessionSendDataMode)

There is nothing wrong with this and you can indeed convert your UIImage to a Data object and send it this way, it will work.

However I don't think you can track progress and MultiPeer does not give you any delegates to track progress using this method.

Instead you are left with two other options. You could use

func session(_ session: MCSession, 
             didFinishReceivingResourceWithName resourceName: String, 
             fromPeer peerID: MCPeerID, 
             at localURL: URL?, 
             withError error: Error?)

Or you could use

func startStream(withName streamName: String, 
                 toPeer peerID: MCPeerID) throws -> OutputStream

I am going to use the first option as that is more straightforward however I think the stream option will give you better results. You can read up on both the options here:

Send Resource (we will implement this one)

Streaming

Step 1

I made some UI updates to your original code by adding a UIImageView to show the transferred image to the advertiser(guest) and a UIButton to start the file transfer from the browser(host) MultiPeer Connectivity File Transfer Progress Sample Project Storyboard

The UIImageView has an outlet named @IBOutlet weak var imageView: UIImageView! and an action for the UIButton @IBAction func sendImageAsResource(_ sender: Any)

I have also added an image called image2.jpg to the project which we will send from the host to the guest.

Step 2

I also declared few additional variables

// Progress variable that needs to store the progress of the file transfer
var fileTransferProgress: Progress?
    
// Timer that will be used to check the file transfer progress
var checkProgressTimer: Timer?

// Used by the host to track bytes to receive
var bytesExpectedToExchange = 0
    
// Used to track the time taken in transfer, this is for testing purposes.
// You might get more reliable results using Date to track time
var transferTimeElapsed = 0.0

Step 3

Set up the host and guest as normal by tapping Guest and Host buttons respectively. After that, tap the Send image as resource button on the host and the action is implemented as follows for the host:

// A new action added to send the image stored in the bundle
@IBAction func sendImageAsResource(_ sender: Any) 
{        
        // Call local function created
        sendImageAsResource()
}

func sendImageAsResource()
{
        // 1. Get the url of the image in the project bundle.
        // Change this if your image is hosted in your documents directory
        // or elsewhere.
        //
        // 2. Get all the connected peers. For testing purposes I am only
        // getting the first peer, you might need to loop through all your
        // connected peers and send the files individually.
        guard let imageURL = Bundle.main.url(forResource: "image2",
                                             withExtension: "jpg"),
              let guestPeerID = mcSession.connectedPeers.first else {
            return
        }
        
        // Retrieve the file size of the image
        if let fileSizeToTransfer = getFileSize(atURL: imageURL)
        {
            bytesExpectedToExchange = fileSizeToTransfer
            
            // Put the file size in a dictionary
            let fileTransferMeta = ["fileSize": bytesExpectedToExchange]
            
            // Convert the dictionary to a data object in order to send it via
            // MultiPeer
            let encoder = JSONEncoder()
            
            if let JSONData = try? encoder.encode(fileTransferMeta)
            {
                // Send the file size to the guest users
                try? mcSession.send(JSONData, toPeers: mcSession.connectedPeers,
                                    with: .reliable)
            }
        }
        
        // Ideally for best reliability, you will want to develop some logic
        // for the guest to respond that it has received the file size and then
        // you should initiate the transfer to that peer only after you receive
        // this confirmation. For now, I just add a delay so that I am highly
        // certain the guest has received this data for testing purposes
        DispatchQueue.main.asyncAfter(deadline: .now() + 1)
        { [weak self] in
            self?.initiateFileTransfer(ofImage: imageURL, to: guestPeerID)
        }
    }
    
func initiateFileTransfer(ofImage imageURL: URL, to guestPeerID: MCPeerID)
{
        // Initialize and fire a timer to check the status of the file
        // transfer every 0.1 second
        checkProgressTimer = Timer.scheduledTimer(timeInterval: 0.1,
                                                  target: self,
                                                  selector: #selector(updateProgressStatus),
                                                  userInfo: nil,
                                                  repeats: true)
        
        // Call the sendResource function and send the image from the bundle
        // keeping hold of the returned progress object which we need to keep checking
        // using the timer
        fileTransferProgress = mcSession.sendResource(at: imageURL,
                                          withName: "image2.jpg",
                                          toPeer: guestPeerID,
                                          withCompletionHandler: { (error) in
                                            
                                            // Handle errors
                                            if let error = error as NSError?
                                            {
                                                print("Error: \(error.userInfo)")
                                                print("Error: \(error.localizedDescription)")
                                            }
                                            
                                          })
}

func getFileSize(atURL url: URL) -> Int?
{
        let urlResourceValue = try? url.resourceValues(forKeys: [.fileSizeKey])
        
        return urlResourceValue?.fileSize
}

Step 4

This next function is used by the host and the guest. The guest will make side of things will make sense later on, however for the host, in step 3, you have stored a progress object after initiating the file transfer and you have launched a timer to fire every 0.1 seconds, so now implement the timer to query this progress object to display the progress and data transfer status on the host side in the UILabel

/// Function fired by the local checkProgressTimer object used to track the progress of the file transfer
/// Function fired by the local checkProgressTimer object used to track the progress of the file transfer
@objc
func updateProgressStatus()
{
        // Update the time elapsed. As mentioned earlier, a more reliable approach
        // might be to compare the time of a Date object from when the
        // transfer started to the time of a current Date object
        transferTimeElapsed += 0.1
        
        // Verify the progress variable is valid
        if let progress = fileTransferProgress
        {
            // Convert the progress into a percentage
            let percentCompleted = 100 * progress.fractionCompleted
            
            // Calculate the data exchanged sent in MegaBytes
            let dataExchangedInMB = (Double(bytesExpectedToExchange)
                                     * progress.fractionCompleted) / 1000000
            
            // We have exchanged 'dataExchangedInMB' MB of data in 'transferTimeElapsed'
            // seconds. So we have to calculate how much data will be exchanged in 1 second
            // using cross multiplication
            // For example:
            // 2 MB in 0.5s
            //  ?   in  1s
            // MB/s = (1 x 2) / 0.5 = 4 MB/s
            let megabytesPerSecond = (1 * dataExchangedInMB) / transferTimeElapsed
            
            // Convert dataExchangedInMB into a string rounded to 2 decimal places
            let dataExchangedInMBString = String(format: "%.2f", dataExchangedInMB)
            
            // Convert megabytesPerSecond into a string rounded to 2 decimal places
            let megabytesPerSecondString = String(format: "%.2f", megabytesPerSecond)
            
            // Update the progress an data exchanged on the UI
            numberLabel.text = "\(percentCompleted.rounded())% - \(dataExchangedInMBString) MB @ \(megabytesPerSecondString) MB/s"
            
            // This is mostly useful on the browser side to check if the file transfer
            // is complete so that we can safely deinit the timer, reset vars and update the UI
            if percentCompleted >= 100
            {
                numberLabel.text = "Transfer complete!"
                checkProgressTimer?.invalidate()
                checkProgressTimer = nil
                transferTimeElapsed = 0.0
            }
        }
}

Step 5

Handle the receiving of the file on the receiver (guest) side by implementing the following delegate methods

func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID)
{
        // Check if the guest has received file transfer data
        if let fileTransferMeta = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Int],
           let fileSizeToReceive = fileTransferMeta["fileSize"]
        {
            // Store the bytes to be received in a variable
            bytesExpectedToExchange = fileSizeToReceive
            print("Bytes expected to receive: \(fileSizeToReceive)")
            return
        }
}

func session(_ session: MCSession,
             didStartReceivingResourceWithName resourceName: String,
             fromPeer peerID: MCPeerID,
             with progress: Progress) 
{
        
        // Store the progress object so that we can query it using the timer
        fileTransferProgress = progress
        
        // Launch the main thread
        DispatchQueue.main.async { [unowned self] in
            
            // Fire the timer to check the file transfer progress every 0.1 second
            self.checkProgressTimer = Timer.scheduledTimer(timeInterval: 0.1,
                                                           target: self,
                                                           selector: #selector(updateProgressStatus),
                                                           userInfo: nil,
                                                           repeats: true)
        }
}

func session(_ session: MCSession,
             didFinishReceivingResourceWithName resourceName: String,
             fromPeer peerID: MCPeerID,
             at localURL: URL?,
             withError error: Error?) 
{
        
        // Verify that we have a valid url. You should get a url to the file in
        // the tmp directory
        if let url = localURL
        {
            // Launch the main thread
            DispatchQueue.main.async { [weak self] in
                
                // Call a function to handle download completion
                self?.handleDownloadCompletion(withImageURL: url)
            }
        }
}

/// Handles the file transfer completion process on the advertiser/client side
    /// - Parameter url: URL of a file in the documents directory
    func handleDownloadCompletion(withImageURL url: URL) 
{ 
        // Debugging data
        print("Full URL: \(url.absoluteString)")
        
        // Invalidate the timer
        checkProgressTimer?.invalidate()
        checkProgressTimer = nil
        
        // Set the UIImageView with the downloaded image
        imageView.image = UIImage(contentsOfFile: url.path)
}

Step 6

Run the code and this is the end result (uploaded to youtube) on the guest side which shows the progress and the file once the transfer is complete and the same progress is shown on the host side as well.

Step 7

I did not implement this but I believe this bit is straight forward:

  1. The file size can be calculated as from the host and it can be sent as a message to the guest on the size to expect

  2. You can can compute the approximate % of a file that has been downloaded by multiplying the progress % by the file size

  3. The speed can be calculated based on the amount of data downloaded / time elapsed so far since the transfer started

I can try to add this code if you feel these calculations are not straightforward.

Update

I have updated the above code samples, github repo and the video to include the final 3 steps as well which gives the final result as follows:

iOS MultiPeer File Transfer with progress, transfer status and transfer speed

Riff answered 12/1, 2022 at 11:0 Comment(11)
Thank you so much for your hard work. Actually Haven't expect such a answer. Understood the things that you mentioned. I check your repo its working fine. I request you if you can do that 3 points because I feel these calculations are not straightforward for me!. Appreciate!Chickpea
I will update it tomorrow for you @YogeshPatelRiff
@YogeshPatel please see my updates in the code, repo and video - I believe it now has everything you need.Riff
Hello @shawn, I am facing one issue when I transfer multiple images then timer will not stop. It showing completed status in loop. any suggestion?Chickpea
@YogeshPatel - can you put this code of transferring on a gist or something and I can try to see if I can helpRiff
I did same thing that you did. When click on sendImageSource you are transferring one image and I am doing multiple but timer is not stoping.Chickpea
One more thing can you update this code with compare date as you comment!Chickpea
Hello @shawn, I able to stop the time. can you remove this transferTimeElapsed and do date compare. I don't have much idea about this any help or any suggestion.Chickpea
sometime it will give accurate file size or not could you check with multiple image sharing on button click. Thank You!Chickpea
@YogeshPatel - I won't be able to look at it any time soon as I am bit busy. Maybe I'll have some time later in the week but can't promise. The idea is you create a Date object startTime as you start the transfer. You still need the timer to keep checking the date object. Instead of the checking the seconds elapsed in the timer function using the transferTimeElapsed counter, you should create another date object currentTime and get the difference in seconds from the startTime. Although besides slightly better accuracy, you don't gain much using the date version.Riff
ok got it I will do this! thanks for your explanation.Chickpea

© 2022 - 2024 — McMap. All rights reserved.