3

I am making an App in Swift that records some audio and then sends that recording to my PHP server.

The App records the audio clip fine (it can be played back with no problem). When I println the recorded audio clip it shows loads and loads of byte data (same when I put the audio into an NSData wrapper). This all suggests to me that the audio is fine inside the app.

The PHP file catching the recording on my server also works fine and without errors.

But somewhere along the line the recorded audio clip gets lost.

Swift code that uploads the recording:

// The variable "recordedFileURL" is defined earlier in the code like this:

currentFilename = "xxxx.m4a"
let dirPaths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
let docsDir: AnyObject=dirPaths[0]
recordedFilePath = docsDir.stringByAppendingPathComponent(self.currentFilename)
recordedFileURL = NSURL(fileURLWithPath: self.recordedFilePath)

// "currentFilename", "recordedFilePath" and "recordedFileURL" are all global variables

// This recording stored at "recordedFileURL" can be played back fine.

let sendToPath = "http://......../catch.php"
let sendToURL = NSURL(string: sendToPath)
let recording: NSData? = NSData(contentsOfURL: recordedFileURL)
let boundary = "--------14737809831466499882746641449----"
let contentType = "multipart/form-data;boundary=\(boundary)"

var request = NSMutableURLRequest()
request.URL = sendToURL
request.HTTPMethod = "POST"
request.addValue(contentType, forHTTPHeaderField: "Content-Type")
request.addValue(recId, forHTTPHeaderField: "REC-ID") // recId is defined elsewhere

var body = NSMutableData()
var header = "Content-Disposition: form-data; name=\"\(currentFilename)\"; filename=\"\(recordedFilePath)\"\r\n"

body.appendData(("\r\n-\(boundary)\r\n" as NSString).dataUsingEncoding(NSUTF8StringEncoding)!)
body.appendData((header as NSString).dataUsingEncoding(NSUTF8StringEncoding)!)
body.appendData(("Content-Type: application/octet-stream\r\n\r\n" as NSString).dataUsingEncoding(NSUTF8StringEncoding)!)

body.appendData(recording!) // adding the recording here

body.appendData(("\r\n-\(boundary)\r\n" as NSString).dataUsingEncoding(NSUTF8StringEncoding)!)

request.HTTPBody = body

var session = NSURLSession.sharedSession()
var task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in

    println("upload complete")
    let dataStr = NSString(data: data, encoding: NSUTF8StringEncoding)
    println(dataStr)

})

task.resume()

PHP code in the file catch.php that is supposed to receive the recording:

$contents = file_get_contents('php://input');
$files = $_FILES;

echo "Caught the following:/r/n";
echo "Contents:" . var_export($contents) . "/r/n";
echo "Files:" . var_export($files) . "/r/n";

And whenever I run all of this I get the following output from catch.php:

Caught the following:
Contents:''
Files:array (
)

So catch.php isn't receiving anything at all.

Am I sending the recording wrong, or am catching the recording wrong? Or both?

Thanks in advance.

Jimmery
  • 9,783
  • 25
  • 83
  • 157
  • do you sure, that `recording` variable contain data when you make `body.appendData(recording!)`? Check it in debugger or put length in NSLog to be sure... – Vitalii Gozhenko Jan 15 '15 at 22:25
  • Did you check max_upload_size in php.ini. If you are trying upload file greater than that value can cause this issue – Jose Antony Jan 16 '15 at 06:47
  • thanks for the responses! php.ini max_upload_size is set to 20Mb (should be enough for our 1-10 second recordings) - and yes, the recording variable contains loads of data - when I NSLog it the debug window is filled with data - so much of it that i have to scroll thru it – Jimmery Jan 16 '15 at 08:54
  • Does `var_dump($contents);` helps you? – John Jan 16 '15 at 12:43
  • `var_dump($contents)` gives the same result as `var_export($content)` – Jimmery Jan 16 '15 at 16:21
  • 2
    "body.appendData(("\r\n!!-!!\(boundary)\r\n" - extra "-" here – maxpovver Jan 17 '15 at 13:47

2 Answers2

4

Your PHP code is mostly fine. The $_FILES part is OK, but php://input is not available with enctype="multipart/form-data".

The problem is with how you are generating the HTTP request in your Swift code. Mainly the HTTP headers. When creating the headers for multipart data, the pattern is this (if we choose AAAAA to be our boundary):

  • Our chosen boundary: "AAAAA"
  • Content Type = "multipart/form-data;boundary=AAAAA"
  • Beginning Bounary = --AAAAA
  • Ending Boundary = --AAAAA--

So by fixing up your code a little bit:

// This was your main problem
let boundary = "--------14737809831466499882746641449----"
let beginningBoundary = "--\(boundary)"
let endingBoundary = "--\(boundary)--"
let contentType = "multipart/form-data;boundary=\(boundary)"

// recordedFilePath is Optional, so the resulting string will end up being 'Optional("/path/to/file/filename.m4a")', which is wrong.
// We could just use currentFilename if we wanted
let filename = recordedFilePath ?? currentFilename
var header = "Content-Disposition: form-data; name=\"\(currentFilename)\"; filename=\"\(recordedFilePath!)\"\r\n"

var body = NSMutableData()
body.appendData(("\(beginningBoundary)\r\n" as NSString).dataUsingEncoding(NSUTF8StringEncoding)!)
body.appendData((header as NSString).dataUsingEncoding(NSUTF8StringEncoding)!)
body.appendData(("Content-Type: application/octet-stream\r\n\r\n" as NSString).dataUsingEncoding(NSUTF8StringEncoding)!)
body.appendData(recording!) // adding the recording here
body.appendData(("\r\n\(endingBoundary)\r\n" as NSString).dataUsingEncoding(NSUTF8StringEncoding)!)

var request = NSMutableURLRequest()
request.URL = sendToURL
request.HTTPMethod = "POST"
request.addValue(contentType, forHTTPHeaderField: "Content-Type")
request.addValue(recId, forHTTPHeaderField: "REC-ID") // recId is defined elsewhere
request.HTTPBody = body

In case you run into things like this again, when debugging networking code like this, I like to use tools that let me inspect the HTTP Network data being transferred. I personally like HTTPScoop because its simple, but you can also use BurpSuite or Charles

I just ran your code through, and compared the HTTP traffic with what happened when I made the request with curl

curl -X POST http://localhost/\~cjwirth/catch.php -F "file=@Untitled.m4a"
cjwirth
  • 2,015
  • 1
  • 15
  • 24
0

here is the swift 4 ~ 5 code. creat new a button(buttonLabel in code) on ViewController and link button action to @IBAction and @IBOutlet [Hold to recode and release to upload audio file]

import UIKit
import AVFoundation

class ViewController2: UIViewController, AVAudioRecorderDelegate{
    
    var recordingSession: AVAudioSession!
    var audioRecorder: AVAudioRecorder!
    var audioPlayer: AVAudioPlayer!
    var numberOfRecords = 0
    

    @IBOutlet weak var buttonLabel: UIButton!
    let E_401 = "E_401"
    let DATABASE_PATH = "http://<IP_address_of_PHP_server>/YourPrjectName/"
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Set the recognizer to recognize the button action
        let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(record))
        longPressRecognizer.minimumPressDuration = 0
        buttonLabel.addGestureRecognizer(longPressRecognizer)
        
        // Setting up session
        recordingSession = AVAudioSession.sharedInstance()
        
        
        // Get permission from user to use mic
        AVAudioSession.sharedInstance().requestRecordPermission{ (hasPermission) in
            if hasPermission
            {print ("ACCEPTED")}
        }
    }
    
    
    @IBAction func record(_ gestureRecognizer: UILongPressGestureRecognizer) {
        
        // Check if we have an active recorder
        if (gestureRecognizer.state == .began) && (audioRecorder == nil) {
            // Increase +1 total number of recordings for every new recording made
            
            self.numberOfRecords += 1
            
            // Setting filename and settings
            let filename = getDirectory().appendingPathComponent("\(numberOfRecords).m4a")
            let settings = [
                AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
                AVSampleRateKey: 12000,
                AVNumberOfChannelsKey: 1,
                AVEncoderAudioQualityKey: AVAudioQuality.medium.rawValue
            ]
            
            do
            {
                // Start audio recording
                buttonLabel.setTitle("Recording...", for: .normal)
                audioRecorder = try AVAudioRecorder(url: filename, settings: settings)
                audioRecorder.delegate = self
                audioRecorder.record()
            }
            catch
            {
                // Catch for errors
                displayAlert(title: "Oops!", message: "Recording failed")
                
            }
            
        } else if gestureRecognizer.state == .ended && (audioRecorder != nil)
            
        {
            // Stopping audio recording
            buttonLabel.setTitle("Start Recording", for: .normal)
            audioRecorder.stop()
            audioRecorder = nil
            
            
            
            do {
                let filename = getDirectory().appendingPathComponent("\(numberOfRecords).m4a")
                let recording: NSData = try NSData(contentsOf: filename)
                self.uploadFile(fileData: recording as Data, fileName: "\(numberOfRecords).m4a"){
                    (fileURL, e) in
                    if e == nil {
                        print("FILE URL: " + fileURL!)
                    }
                }
                
            } catch {
                print("Unexpected <<<<<<<<<<<<<<>>>>>>>>>>>>>> error: \(error)")
            }

        }
    }
    
    // Function that gets path to directory
    func getDirectory () -> URL
    {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        let documentDirectory = paths[0]
        
        return documentDirectory
    }
    
    // Function that displays an alert
    func displayAlert(title:String, message:String)
    {
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "dismiss", style: .default, handler: nil))
        present(alert, animated: true, completion: nil)
    }
    
    
    func uploadFile(fileData:Data, fileName:String , completion: @escaping (_ fileURL:String?, _ error:String?) -> Void) {
        let recId = "\(numberOfRecords)"
        print("FILENAME: \(fileName)")
        let request = NSMutableURLRequest()

        let boundary = "--------14737809831466499882746641449----"
        let beginningBoundary = "--\(boundary)"
        let endingBoundary = "--\(boundary)--"
        let contentType = "multipart/form-data;boundary=\(boundary)"
        
        
        
        request.url = URL(string: DATABASE_PATH + "catch.php")
//        catch.php is php script on server
        request.httpShouldHandleCookies = false
        request.timeoutInterval = 60
        request.httpMethod = "POST"
        request.setValue(contentType, forHTTPHeaderField: "Content-Type")
        let body = NSMutableData()
        body.append("--\(boundary)\r\n".data(using: String.Encoding.utf8)!)
        body.append("Content-Disposition: form-data; name=\"fileName\"\r\n\r\n".data(using: String.Encoding.utf8)!)
        body.append("\(fileName)\r\n".data(using: String.Encoding.utf8)!)
        body.append("--\(boundary)\r\n".data(using: String.Encoding.utf8)!)
        body.append("Content-Disposition: form-data; name=\"file\"; filename=\"file\"\r\n".data(using: String.Encoding.utf8)!)
        
        body.append(("\(beginningBoundary)\r\n" as NSString).data(using: String.Encoding.utf8.rawValue)!)
        body.append(("Content-Type: application/octet-stream\r\n\r\n" as NSString).data(using: String.Encoding.utf8.rawValue)!)

        body.append(fileData)
        body.append("\r\n".data(using: String.Encoding.utf8)!)
        
        
        body.append("--\(boundary)--\r\n".data(using: String.Encoding.utf8)!)
        request.addValue(contentType, forHTTPHeaderField: "Content-Type")
//        request.addValue(recId, forHTTPHeaderField: "REC-ID")
        request.httpBody = body as Data
        
        
        let session = URLSession.shared
        let task = session.dataTask(with: request as URLRequest) { (data, response, error) in
            guard let _:Data = data as Data?, let _:URLResponse = response, error == nil else {
                DispatchQueue.main.async { completion(nil, error!.localizedDescription) }
                return
            }
            if let response = String(data: data!, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue)) {
                print("XSUploadFile -> RESPONSE: " + self.DATABASE_PATH + response)
                DispatchQueue.main.async { completion(self.DATABASE_PATH + response, nil) }
                
                // NO response
            } else { DispatchQueue.main.async { completion(nil, self.E_401) } }// ./ If response
        }; task.resume()
    }
}
HomiFox
  • 123
  • 8