Gcore named a Leader in the GigaOm Radar for AI Infrastructure!Get the report
  1. Home
  2. Blog
  3. How to add a VOD uploading feature to your iOS app in 15 minutes
Network
Developers

How to add a VOD uploading feature to your iOS app in 15 minutes

  • March 30, 2023
  • 13 min read
How to add a VOD uploading feature to your iOS app in 15 minutes

This is a step-by-step guide on Gcore’s solution for adding a new VOD feature to your iOS application in 15 minutes. The feature allows users to record videos from their phone, upload videos to storage, and play videos in the player inside the app.

Here is what the result will look like:

This is part of a series of guides about adding new video features to an iOS application. In other articles, we show you how to create a mobile streaming app on iOS, and how to add video call and smooth scrolling VOD features to an existing app.

What functions you can add with the help of this guide

The solution includes the following:

  • Recording: Local video recording directly from the device’s camera; gaining access to the camera and saving raw video to internal storage.
  • Uploading to the server: Uploading the recorded video to cloud video hosting, uploading through TUSclient, async uploading, and getting a link to the processed video.
  • List of videos: A list of uploaded videos with screenshot covers and text descriptions.
  • Player: Playback of the selected video in AVPlayer with ability to cache, play with adaptive bitrate of HLS, rewind, etc.

How to add the VOD feature

Step 1: Permissions

The project uses additional access rights that need to be specified. These are:

  • NSMicrophoneUsageDescription (Privacy: Microphone Usage Description)
  • NSCameraUsageDescription (Privacy: Camera Usage Description).

Step 2: Authorization

You’ll need a Gcore account, which can be created in just 1 minute at gcore.com. You won’t need to pay anything; you can test the solution with a free plan.

To use Gcore services, you’ll need an access token, which comes in the server’s response to the authentication request. Here’s how to get it:

1. Create a model that will come from the server.

Plaintext
struct Tokens: Decodable { let refresh: String let access: String }

2. Create a common protocol for your requests.

Plaintext
protocol DataRequest { associatedtype Response var url: String { get } var method: HTTPMethod { get } var headers: [String : String] { get } var queryItems: [String : String] { get } var body: Data? { get } var contentType: String { get } func decode(_ data: Data) throws -> Response } extension DataRequest where Response: Decodable { func decode(_ data: Data) throws -> Response { let decoder = JSONDecoder() return try decoder.decode(Response.self, from: data) } } extension DataRequest { var contentType: String { "application/json" } var headers: [String : String] { [:] } var queryItems: [String : String] { [:] } var body: Data? { nil } }

3. Create an authentication request.

Plaintext
struct AuthenticationRequest: DataRequest { typealias Response = Tokens let username: String let password: String var url: String { GсoreAPI.authorization.rawValue } var method: HTTPMethod { .post } var body: Data? { try? JSONEncoder().encode([ "password": password, "username": username, ]) } }

4. Then you can use the request in any part of the application, using your preferred approach for your internet connection. For example:

Plaintext
func signOn(username: String, password: String) { let request = AuthenticationRequest(username: username, password: password) let communicator = HTTPCommunicator() communicator.request(request) { [weak self] result in switch result { case .success(let tokens): Settings.shared.refreshToken = tokens.refresh Settings.shared.accessToken = tokens.access Settings.shared.username = username Settings.shared.userPassword = password DispatchQueue.main.async { self?.view.window?.rootViewController = MainController() } case .failure(let error): self?.errorHandle(error) } } }

Step 3: Setting up the camera session

In mobile apps on iOS systems, the AVFoundation framework is used to work with the camera. Let’s create a class that will work with the camera at a lower level.

1. Create a protocol in order to send the path to the recorded fragment and its time to the controller, as well as the enumeration of errors that may occur during initialization. The most common error is that the user did not grant the rights for camera use.

Plaintext
import Foundation import AVFoundation enum CameraSetupError: Error { case accessDevices, initializeCameraInputs } protocol CameraDelegate: AnyObject { func addRecordedMovie(url: URL, time: CMTime) }

2. Create the camera class with properties and initializer.

Plaintext
final class Camera: NSObject { var movieOutput: AVCaptureMovieFileOutput! weak var delegate: CameraDelegate? private var videoDeviceInput: AVCaptureDeviceInput! private var rearCameraInput: AVCaptureDeviceInput! private var frontCameraInput: AVCaptureDeviceInput! private let captureSession: AVCaptureSession // There may be errors during initialization, if this happens, the initializer throws an error to the controller init(captureSession: AVCaptureSession) throws { self.captureSession = captureSession //check access to devices and setup them guard let rearCamera = AVCaptureDevice.default(for: .video), let frontCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front), let audioInput = AVCaptureDevice.default(for: .audio) else { throw CameraSetupError.accessDevices } do { let rearCameraInput = try AVCaptureDeviceInput(device: rearCamera) let frontCameraInput = try AVCaptureDeviceInput(device: frontCamera) let audioInput = try AVCaptureDeviceInput(device: audioInput) let movieOutput = AVCaptureMovieFileOutput() if captureSession.canAddInput(rearCameraInput), captureSession.canAddInput(audioInput), captureSession.canAddInput(frontCameraInput), captureSession.canAddOutput(movieOutput) { captureSession.addInput(rearCameraInput) captureSession.addInput(audioInput) self.videoDeviceInput = rearCameraInput self.rearCameraInput = rearCameraInput self.frontCameraInput = frontCameraInput captureSession.addOutput(movieOutput) self.movieOutput = movieOutput } } catch { throw CameraSetupError.initializeCameraInputs } }

3. Create methods. Depending on user’s actions on the UI layer, the controller will call the corresponding method.

Plaintext
func flipCamera() { guard let rearCameraIn = rearCameraInput, let frontCameraIn = frontCameraInput else { return } if captureSession.inputs.contains(rearCameraIn) { captureSession.removeInput(rearCameraIn) captureSession.addInput(frontCameraIn) } else { captureSession.removeInput(frontCameraIn) captureSession.addInput(rearCameraIn) } } func stopRecording() { if movieOutput.isRecording { movieOutput.stopRecording() } } func startRecording() { if movieOutput.isRecording == false { guard let outputURL = temporaryURL() else { return } movieOutput.startRecording(to: outputURL, recordingDelegate: self) DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in guard let self = self else { return } self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.updateTime), userInfo: nil, repeats: true) self.timer?.fire() } } else { stopRecording() } }

4. To save a video fragment in memory, you will need a path for it. This method returns this path:

Plaintext
// Creating a temporary storage for the recorded video fragment private func temporaryURL() -> URL? { let directory = NSTemporaryDirectory() as NSString if directory != "" { let path = directory.appendingPathComponent(UUID().uuidString + ".mov") return URL(fileURLWithPath: path) } return nil } }

5. Subscribe to the protocol in order to send the path to the controller.

Plaintext
//MARK: - AVCaptureFileOutputRecordingDelegate //When the shooting of one clip ends, it sends a link to the file to the delegate extension Camera: AVCaptureFileOutputRecordingDelegate { func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) { if let error = error { print("Error recording movie: \(error.localizedDescription)") } else { delegate?.addRecordedMovie(url: outputFileURL, time: output.recordedDuration) } } }

Step 4: Layout for the camera

Create a class that will control the camera on the UI level. The user will transmit commands through this class, and it will send its delegate to send the appropriate commands to the preceding class.

Note: You will need to add your own icons or use existing ones in iOS.

1. Create a protocol so that your view can inform the controller about user actions.

Plaintext
protocol CameraViewDelegate: AnyObject { func tappedRecord(isRecord: Bool) func tappedFlipCamera() func tappedUpload() func tappedDeleteClip() func shouldRecord() -> Bool }

2. Create the camera view class and initialize the necessary properties.

Plaintext
final class CameraView: UIView { var isRecord = false { didSet { if isRecord { recordButton.setImage(UIImage(named: "pause.icon"), for: .normal) } else { recordButton.setImage(UIImage(named: "play.icon"), for: .normal) } } } var previewLayer: AVCaptureVideoPreviewLayer? weak var delegate: CameraViewDelegate? let recordButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "play.icon"), for: .normal) button.imageView?.contentMode = .scaleAspectFit button.addTarget(self, action: #selector(tapRecord), for: .touchUpInside) button.translatesAutoresizingMaskIntoConstraints = false return button }() let flipCameraButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "flip.icon"), for: .normal) button.imageView?.contentMode = .scaleAspectFit button.addTarget(self, action: #selector(tapFlip), for: .touchUpInside) button.translatesAutoresizingMaskIntoConstraints = false return button }() let uploadButton: UIButton = { let button = UIButton() button.setImage(UIImage(named: "upload.icon"), for: .normal) button.imageView?.contentMode = .scaleAspectFit button.addTarget(self, action: #selector(tapUpload), for: .touchUpInside) button.translatesAutoresizingMaskIntoConstraints = false return button }() let clipsLabel: UILabel = { let label = UILabel() label.textColor = .white label.font = .systemFont(ofSize: 14) label.textAlignment = .left label.text = "Clips: 0" return label }() let deleteLastClipButton: Button = { let button = Button() button.setTitle("", for: .normal) button.setImage(UIImage(named: "delete.left.fill"), for: .normal) button.addTarget(self, action: #selector(tapDeleteClip), for: .touchUpInside) return button }() let recordedTimeLabel: UILabel = { let label = UILabel() label.text = "0s / \(maxRecordTime)s" label.font = .systemFont(ofSize: 14) label.textColor = .white label.textAlignment = .left return label }() }

3. Since the view will show the image from the device’s camera, you need to link it to the session and configure it.

Plaintext
func setupLivePreview(session: AVCaptureSession) { let previewLayer = AVCaptureVideoPreviewLayer.init(session: session) self.previewLayer = previewLayer previewLayer.videoGravity = .resizeAspectFill previewLayer.connection?.videoOrientation = .portrait layer.addSublayer(previewLayer) session.startRunning() backgroundColor = .black } // when the size of the view is calculated, we transfer this size to the image from the camera override func layoutSubviews() { previewLayer?.frame = bounds }

4. Create a layout for UI elements.

Plaintext
private func initLayout() { [clipsLabel, deleteLastClipButton, recordedTimeLabel].forEach { $0.translatesAutoresizingMaskIntoConstraints = false addSubview($0) } NSLayoutConstraint.activate([ flipCameraButton.topAnchor.constraint(equalTo: topAnchor, constant: 10), flipCameraButton.rightAnchor.constraint(equalTo: rightAnchor, constant: -10), flipCameraButton.widthAnchor.constraint(equalToConstant: 30), flipCameraButton.widthAnchor.constraint(equalToConstant: 30), recordButton.centerXAnchor.constraint(equalTo: centerXAnchor), recordButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -5), recordButton.widthAnchor.constraint(equalToConstant: 30), recordButton.widthAnchor.constraint(equalToConstant: 30), uploadButton.leftAnchor.constraint(equalTo: recordButton.rightAnchor, constant: 20), uploadButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -5), uploadButton.widthAnchor.constraint(equalToConstant: 30), uploadButton.widthAnchor.constraint(equalToConstant: 30), clipsLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 5), clipsLabel.centerYAnchor.constraint(equalTo: uploadButton.centerYAnchor), deleteLastClipButton.centerYAnchor.constraint(equalTo: clipsLabel.centerYAnchor), deleteLastClipButton.rightAnchor.constraint(equalTo: recordButton.leftAnchor, constant: -15), deleteLastClipButton.widthAnchor.constraint(equalToConstant: 30), deleteLastClipButton.widthAnchor.constraint(equalToConstant: 30), recordedTimeLabel.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor), recordedTimeLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 5) ]) }

The result of the layout will look like this:

5. Add the initializer. The controller will transfer the session in order to access the image from the camera:

Plaintext
convenience init(session: AVCaptureSession) { self.init(frame: .zero) setupLivePreview(session: session) addSubview(recordButton) addSubview(flipCameraButton) addSubview(uploadButton) initLayout()

6. Create methods that will work when the user clicks on the buttons.

Plaintext
@objc func tapRecord() { guard delegate?.shouldRecord() == true else { return } isRecord = !isRecord delegate?.tappedRecord(isRecord: isRecord) } @objc func tapFlip() { delegate?.tappedFlipCamera() } @objc func tapUpload() { delegate?.tappedUpload() } @objc func tapDeleteClip() { delegate?.tappedDeleteClip() } }

Step 5: Interaction with recorded fragments

On an iPhone, the camera records video in fragments. When the user decides to upload the video, you need to collect its fragments into one file and send it to the server. Create another class that will do this command.

Note: When creating a video, an additional file will be created. This file will collect all the fragments, but at the same time, these fragments will remain in the memory until the line-up is completed. In the worst case, it can cause a lack of memory and crash from the application. To avoid this, we recommend limiting the recording time allowed.

Plaintext
import Foundation import AVFoundation final class VideoCompositionWriter: NSObject { private func merge(recordedVideos: [AVAsset]) -> AVMutableComposition { // create empty composition and empty video and audio tracks let mainComposition = AVMutableComposition() let compositionVideoTrack = mainComposition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) let compositionAudioTrack = mainComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) // to correct video orientation compositionVideoTrack?.preferredTransform = CGAffineTransform(rotationAngle: .pi / 2) // add video and audio tracks from each asset to our composition (across compositionTrack) var insertTime = CMTime.zero for i in recordedVideos.indices { let video = recordedVideos[i] let duration = video.duration let timeRangeVideo = CMTimeRangeMake(start: CMTime.zero, duration: duration) let trackVideo = video.tracks(withMediaType: .video)[0] let trackAudio = video.tracks(withMediaType: .audio)[0] try! compositionVideoTrack?.insertTimeRange(timeRangeVideo, of: trackVideo, at: insertTime) try! compositionAudioTrack?.insertTimeRange(timeRangeVideo, of: trackAudio, at: insertTime) insertTime = CMTimeAdd(insertTime, duration) } return mainComposition } /// Combines all recorded clips into one file func mergeVideo(_ documentDirectory: URL, filename: String, clips: [URL], completion: @escaping (Bool, URL?) -> Void) { var assets: [AVAsset] = [] var totalDuration = CMTime.zero for clip in clips { let asset = AVAsset(url: clip) assets.append(asset) totalDuration = CMTimeAdd(totalDuration, asset.duration) } let mixComposition = merge(recordedVideos: assets) let url = documentDirectory.appendingPathComponent("link_\(filename)") guard let exporter = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality) else { return } exporter.outputURL = url exporter.outputFileType = .mp4 exporter.shouldOptimizeForNetworkUse = true exporter.exportAsynchronously { DispatchQueue.main.async { if exporter.status == .completed { completion(true, exporter.outputURL) } else { completion(false, nil) } } } } }

Step 6: Metadata for the videos

There is a specific set of actions for video uploading:

  1. Recording a video
  2. Using your token and the name of the future video, creating a request to the server to create a container for the video file
  3. Getting the usual VOD data in the response
  4. Sending a request for metadata using the token and the VOD ID
  5. Getting metadata in the response
  6. Uploading the video via TUSKit using metadata

Create requests with models. You will use the Decodable protocol from Apple with the enumeration of Coding Keys for easier data parsing.

1. Create a model for VOD, which will contain the data that you need.

Plaintext
struct VOD: Decodable { let name: String let id: Int let screenshot: URL? let hls: URL? enum CodingKeys: String, CodingKey { case name, id, screenshot case hls = "hls_url" } }

2. Create a CreateVideoRequest in order to create an empty container for the video on the server. The VOD model will come in response.

Plaintext
struct CreateVideoRequest: DataRequest { typealias Response = VOD let token: String let videoName: String var url: String { GсoreAPI.videos.rawValue } var method: HTTPMethod { .post } var headers: [String: String] { [ "Authorization" : "Bearer \(token)" ] } var body: Data? { try? JSONEncoder().encode([ "name": videoName ]) } }

3. Create a VideoMetadata model that will contain data for uploading videos from the device to the server and the corresponding request for it.

Plaintext
struct VideoMetadata: Decodable { struct Server: Decodable { let hostname: String } struct Video: Decodable { let name: String let id: Int let clientID: Int enum CodingKeys: String, CodingKey { case name, id case clientID = "client_id" } } let servers: [Server] let video: Video let token: String var uploadURLString: String { "https://" + (servers.first?.hostname ?? "") + "/upload" } } // MARK: Request struct VideoMetadataRequest: DataRequest { typealias Response = VideoMetadata let token: String let videoId: Int var url: String { GсoreAPI.videos.rawValue + "/\(videoId)/" + "upload" } var method: HTTPMethod { .get } var headers: [String: String] { [ "Authorization" : "Bearer \(token)" ] } }

Step 7: Putting the pieces together

We’ve used the code from our demo application as an example. The controller class is described here with a custom view. It will link the camera and the UI as well as take responsibility for creating requests to obtain metadata and then upload the video to the server.

Create View Controller. It will display the camera view and TextField for the video title. This controller has various states (upload, error, common).

MainView

First, create the view.

1. Create a delegate protocol to handle changing the name of the video.

Plaintext
protocol UploadMainViewDelegate: AnyObject { func videoNameDidUpdate(_ name: String) }

2. Create the view and initialize all UI elements except the camera view. It will be added by the controller.

Plaintext
final class UploadMainView: UIView { enum State { case upload, error, common } var cameraView: CameraView? { didSet { initLayoutForCameraView() } } var state: State = .common { didSet { switch state { case .upload: showUploadState() case .error: showErrorState() case .common: showCommonState() } } } weak var delegate: UploadMainViewDelegate? }

3. Add the initialization of UI elements here, except for the camera view. It will be added by the controller.

Plaintext
let videoNameTextField = TextField(placeholder: "Enter the name video") let accessCaptureFailLabel: UILabel = { let label = UILabel() label.text = NSLocalizedString("Error!\nUnable to access capture devices.", comment: "") label.textColor = .black label.numberOfLines = 2 label.isHidden = true label.textAlignment = .center return label }() let uploadIndicator: UIActivityIndicatorView = { let indicator = UIActivityIndicatorView(style: .gray) indicator.transform = CGAffineTransform(scaleX: 2, y: 2) return indicator }() let videoIsUploadingLabel: UILabel = { let label = UILabel() label.text = NSLocalizedString("video is uploading", comment: "") label.font = UIFont.systemFont(ofSize: 16) label.textColor = .gray label.isHidden = true return label }()

4. Create a layout for the elements. Since the camera will be added after, its layout is taken out in a separate method.

Plaintext
private func initLayoutForCameraView() { guard let cameraView = cameraView else { return } cameraView.translatesAutoresizingMaskIntoConstraints = false insertSubview(cameraView, at: 0) NSLayoutConstraint.activate([ cameraView.leftAnchor.constraint(equalTo: leftAnchor), cameraView.topAnchor.constraint(equalTo: topAnchor), cameraView.rightAnchor.constraint(equalTo: rightAnchor), cameraView.bottomAnchor.constraint(equalTo: videoNameTextField.topAnchor), ]) } private func initLayout() { let views = [videoNameTextField, accessCaptureFailLabel, uploadIndicator, videoIsUploadingLabel] views.forEach { $0.translatesAutoresizingMaskIntoConstraints = false addSubview($0) } let keyboardBottomConstraint = videoNameTextField.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor) self.keyboardBottomConstraint = keyboardBottomConstraint NSLayoutConstraint.activate([ keyboardBottomConstraint, videoNameTextField.heightAnchor.constraint(equalToConstant: videoNameTextField.intrinsicContentSize.height + 20), videoNameTextField.leftAnchor.constraint(equalTo: leftAnchor), videoNameTextField.rightAnchor.constraint(equalTo: rightAnchor), accessCaptureFailLabel.centerYAnchor.constraint(equalTo: centerYAnchor), accessCaptureFailLabel.centerXAnchor.constraint(equalTo: centerXAnchor), uploadIndicator.centerYAnchor.constraint(equalTo: centerYAnchor), uploadIndicator.centerXAnchor.constraint(equalTo: centerXAnchor), videoIsUploadingLabel.centerXAnchor.constraint(equalTo: centerXAnchor), videoIsUploadingLabel.topAnchor.constraint(equalTo: uploadIndicator.bottomAnchor, constant: 20) ]) }

5. To show different states, create methods responsible for this.

Plaintext
private func showUploadState() { videoNameTextField.isHidden = true uploadIndicator.startAnimating() videoIsUploadingLabel.isHidden = false accessCaptureFailLabel.isHidden = true cameraView?.recordButton.setImage(UIImage(named: "play.icon"), for: .normal) cameraView?.isHidden = true } private func showErrorState() { accessCaptureFailLabel.isHidden = false videoNameTextField.isHidden = true uploadIndicator.stopAnimating() videoIsUploadingLabel.isHidden = true cameraView?.isHidden = true } private func showCommonState() { videoNameTextField.isHidden = false uploadIndicator.stopAnimating() videoIsUploadingLabel.isHidden = true accessCaptureFailLabel.isHidden = true cameraView?.isHidden = false }

6. Add methods and a variable for the correct processing of keyboard behavior. The video title input field must always be visible.

Plaintext
private var keyboardBottomConstraint: NSLayoutConstraint? private func addObserver() { [UIResponder.keyboardWillShowNotification, UIResponder.keyboardWillHideNotification].forEach { NotificationCenter.default.addObserver( self, selector: #selector(keybordChange), name: $0, object: nil ) } } @objc private func keybordChange(notification: Notification) { guard let keyboardFrame = notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? NSValue, let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return } let keyboardHeight = keyboardFrame.cgRectValue.height - safeAreaInsets.bottom if notification.name == UIResponder.keyboardWillShowNotification { self.keyboardBottomConstraint?.constant -= keyboardHeight UIView.animate(withDuration: duration) { self.layoutIfNeeded() } } else { self.keyboardBottomConstraint?.constant += keyboardHeight UIView.animate(withDuration: duration) { self.layoutIfNeeded() } } }

7. Rewrite the initializer. In deinit, unsubscribe from notifications related to the keyboard.

Plaintext
override init(frame: CGRect) { super.init(frame: frame) initLayout() backgroundColor = .white videoNameTextField.delegate = self addObserver() } required init?(coder: NSCoder) { super.init(coder: coder) initLayout() backgroundColor = .white videoNameTextField.delegate = self addObserver() } deinit { NotificationCenter.default.removeObserver(self) }

8. Sign the view under UITextFieldDelegate to intercept the necessary actions related to TextField.

Plaintext
extension UploadMainView: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { delegate?.videoNameDidUpdate(textField.text ?? "") return textField.resignFirstResponder() } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { guard let text = textField.text, text.count < 21 else { return false } return true } }

Controller

Create ViewController.

1. Specify the necessary variables and configure the controller.

Plaintext
final class UploadController: BaseViewController { private let mainView = UploadMainView() private var camera: Camera? private var captureSession = AVCaptureSession() private var filename = "" private var writingVideoURL: URL! private var clips: [(URL, CMTime)] = [] { didSet { mainView.cameraView?.clipsLabel.text = "Clips: \(clips.count)" } } private var isUploading = false { didSet { mainView.state = isUploading ? .upload : .common } } // replacing the default view with ours override func loadView() { mainView.delegate = self view = mainView } // initialize the camera and the camera view override func viewDidLoad() { super.viewDidLoad() do { camera = try Camera(captureSession: captureSession) camera?.delegate = self mainView.cameraView = CameraView(session: captureSession) mainView.cameraView?.delegate = self } catch { debugPrint((error as NSError).description) mainView.state = .error } } }

2. Add methods that will respond to clicks of the upload button on View. For this, create a full video from small fragments, create an empty container on the server, get metadata, and then upload the video.

Plaintext
// used then user tap upload button private func mergeSegmentsAndUpload() { guard !isUploading, let camera = camera else { return } isUploading = true camera.stopRecording() if let directoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { let clips = clips.map { $0.0 } // Create a full video from clips VideoCompositionWriter().mergeVideo(directoryURL, filename: "\(filename).mp4", clips: clips) { [weak self] success, outURL in guard let self = self else { return } if success, let outURL = outURL { clips.forEach { try? FileManager.default.removeItem(at: $0) } self.clips = [] let videoData = try! Data.init(contentsOf: outURL) let writingURL = FileManager.default.temporaryDirectory.appendingPathComponent(outURL.lastPathComponent) try! videoData.write(to: writingURL) self.writingVideoURL = writingURL self.createVideoPlaceholderOnServer() } else { self.isUploading = false self.mainView.state = .common self.present(self.createAlert(), animated: true) } } } } // used to send createVideo request private func createVideoPlaceholderOnServer() { guard let token = Settings.shared.accessToken else { refreshToken() return } let http = HTTPCommunicator() let request = CreateVideoRequest(token: token, videoName: filename) http.request(request) { [weak self] result in guard let self = self else { return } switch result { case .success(let vod): self.loadMetadataFor(vod: vod) case .failure(let error): if let error = error as? ErrorResponse, error == .invalidToken { Settings.shared.accessToken = nil self.refreshToken() } else { self.errorHandle(error) } } } } // Requesting the necessary data from the server func loadMetadataFor(vod: VOD) { guard let token = Settings.shared.accessToken else { refreshToken() return } let http = HTTPCommunicator() let request = VideoMetadataRequest(token: token, videoId: vod.id) http.request(request) { [weak self] result in guard let self = self else { return } switch result { case .success(let metadata): self.uploadVideo(with: metadata) case .failure(let error): if let error = error as? ErrorResponse, error == .invalidToken { Settings.shared.accessToken = nil self.refreshToken() } else { self.errorHandle(error) } } } } // Uploading our video to the server via TUSKit func uploadVideo(with metadata: VideoMetadata) { var config = TUSConfig(withUploadURLString: metadata.uploadURLString) config.logLevel = .All TUSClient.setup(with: config) TUSClient.shared.delegate = self let upload: TUSUpload = TUSUpload(withId: filename, andFilePathURL: writingVideoURL, andFileType: ".mp4") upload.metadata = [ "filename" : filename, "client_id" : String(metadata.video.clientID), "video_id" : String(metadata.video.id), "token" : metadata.token ] TUSClient.shared.createOrResume(forUpload: upload) }

3. Subscribe to the TUSDelegate protocol to track errors and successful downloads. It can also be used to display the progress of video downloads.

Plaintext
//MARK: - TUSDelegate extension UploadController: TUSDelegate { func TUSProgress(bytesUploaded uploaded: Int, bytesRemaining remaining: Int) { } func TUSProgress(forUpload upload: TUSUpload, bytesUploaded uploaded: Int, bytesRemaining remaining: Int) { } func TUSFailure(forUpload upload: TUSUpload?, withResponse response: TUSResponse?, andError error: Error?) { if let error = error { print((error as NSError).description) } present(createAlert(), animated: true) mainView.state = .common } func TUSSuccess(forUpload upload: TUSUpload) { let alert = createAlert(title: "Upload success") present(alert, animated: true) mainView.state = .common } }

4. Subscribe to the protocols of the MainView, the camera, and the camera view in order to correctly link all the work of the module.

Plaintext
//MARK: - extensions CameraViewDelegate, CameraDelegate extension UploadController: CameraViewDelegate, CameraDelegate { func updateCurrentRecordedTime(_ time: CMTime) { currentRecordedTime = time.seconds } func tappedDeleteClip() { guard let lastClip = clips.last else { return } lastRecordedTime -= lastClip.1.seconds clips.removeLast() } func addRecordedMovie(url: URL, time: CMTime) { lastRecordedTime += time.seconds clips += [(url, time)] } func shouldRecord() -> Bool { totalRecordedTime < maxRecordTime } func tappedRecord(isRecord: Bool) { isRecord ? camera?.startRecording() : camera?.stopRecording() } func tappedUpload() { guard !clips.isEmpty && filename != "" else { return } mergeSegmentsAndUpload() } func tappedFlipCamera() { camera?.flipCamera() } } extension UploadController: UploadMainViewDelegate { // used then user change name video in view func videoNameDidUpdate(_ name: String) { filename = name }

This was the last step; the job is done! The new feature has been added to your app and configured.

Result

Now you have a full-fledged module for recording and uploading videos.

Conclusion

Through this guide, you’ve learned how to add a VOD uploading feature to your iOS application. We hope this solution will satisfy your needs and delight your users with new options.

Also, we invite you to take a look at our demo application. You will see the result of setting up the VOD viewing for an iOS project.

Table of contents

Try Gcore Network

Gcore all-in-one platform: cloud, AI, CDN, security, and other infrastructure services.

Related articles

Gcore partners with AVEQ to elevate streaming performance monitoring

At Gcore, delivering exceptional streaming experiences to users across our global network is at the heart of what we do. We're excited to share how we're taking our CDN performance monitoring to new heights through our partnership with AVEQ and their innovative Surfmeter solution.Operating a massive global network spanning 210 points of presence across six continents comes with unique challenges. While our globally distributed caching infrastructure already ensures optimal performance for end-users, we recognized the need for deeper insights into the complex interactions between applications and our underlying network. We needed to move beyond traditional server-side monitoring to truly understand what our customers' users experience in the real world.Real-world performance visibilityThat's where AVEQ's Surfmeter comes in. We're now using Surfmeter to gain unprecedented visibility into our network performance through automated, active measurements that simulate actual streaming video quality, exactly as end-users experience it.This isn't about checking boxes or reviewing server logs. It's about measuring what users see on their screens at home. With Surfmeter, our engineering teams can identify and resolve potential bottlenecks or suboptimal configurations, and collaborate more effectively with our customers to continuously improve Quality of Experience (QoE).How we use SurfmeterAVEQ helps us simulate and analyze real-world scenarios where users access different video streams. Their software runs both on network nodes close to our data center CDN caches and at selected end-user locations with genuine ISP connections.What sets Surfmeter apart is its authentic approach: it opens video streams from the same platforms and players that end-users rely on, ensuring measurements truly represent real-world conditions. Unlike monitoring solutions that simply check stream availability, Surfmeter doesn't make assumptions or use third-party playback engines. Instead, it precisely replicates how video players request and decode data served from our CDN.Rapid issue resolutionWhen performance issues arise, Surfmeter provides our engineers with the deep insights needed to quickly identify root causes. Whether the problem lies within our CDN, with peering partners, or on the server side, we can pinpoint it with precision.By monitoring individual video requests, including headers and timings, and combining this data with our internal logging, we gain complete visibility and observability into our entire streaming pipeline. Surfmeter can also perform ping and traceroute tests from the same device, measuring video QoE, allowing our engineers to access all collected data through one API rather than manually connecting to devices for troubleshooting.Competitive benchmarking and future capabilitiesSurfmeter also enables us to benchmark our performance against other services and network providers. By deploying Surfmeter probes at customer-like locations, we can measure streaming from any source via different ISPs.This partnership reflects our commitment to transparency and data-driven service excellence. By leveraging AVEQ's Surfmeter solution, we ensure that our customers receive the best possible streaming performance, backed by objective, end-user-centric insights.Learn more about Gcore CDN

How we engineered a single pipeline for LL-HLS and LL-DASH

Viewers in sports, gaming, and interactive events expect real-time, low-latency streaming experiences. To deliver this, the industry has rallied around two powerful protocols: Low-Latency HLS (LL-HLS) and Low-Latency DASH (LL-DASH).While they share a goal, their methods are fundamentally different. LL-HLS delivers video in a sequence of tiny, discrete files. LL-DASH delivers it as a continuous, chunked download of a larger file. This isn't just a minor difference in syntax; it implies completely different behaviors for the packager, the CDN, and the player.This duality presents a major architectural challenge: How do you build a single, efficient, and cost-effective pipeline that can serve both protocols simultaneously from one source?At Gcore, we took on this unification problem. The result is a robust, single-source pipeline that delivers streams with a glass-to-glass latency of approximately 2.0 seconds for LL-DASH and 3.0 seconds for LL-HLS. This is the story of how we designed it.Understanding the dualityTo build a unified system, we first had to deeply understand the differences in how each protocol operates at the delivery level.LL-DASH: the continuous feedMPEG-DASH has always been flexible, using a single manifest file to define media segments by their timing. Low-Latency DASH builds on this by using Chunked CMAF segments.Imagine a file that is still being written to on the server. Instead of waiting for the whole file to be finished, the player can request it, and the server can send it piece by piece using Chunked Transfer Encoding. The player receives a continuous stream of bytes and can start playback as soon as it has enough data.Single, long-lived files: A segment might be 2–6 seconds long, but it’s delivered as it’s being generated.Timing-based requests: The player knows when a segment should be available and requests it. The server uses chunked transfer to send what it has so far.Player-driven latency: The manifest contains a targetLatency attribute, giving the player a strong hint about how close to the live edge it should play.LL-HLS: the rapid-fire deliveryLL-HLS takes a different approach. It extends the traditional playlist-based HLS by breaking segments into even smaller chunks called Parts.Think of it like getting breaking news updates. The server pre-announces upcoming Parts in the manifest before they are fully available. The player then requests a Part, but the server holds that request open until the Part is ready to be delivered at full speed. This is called a Blocking Playlist Reload.Many tiny files (Parts): A 2-second segment might be broken into four 0.5-second Parts, each requested individually.Manifest-driven updates: The server constantly updates the manifest with new Parts, and uses special tags like #EXT-X-PART-INF and #EXT-X-SERVER-CONTROL to manage delivery.Server-enforced timing: The server controls when the player receives data by holding onto requests, which helps synchronize all viewers.A simplified diagram visually comparing the LL-HLS delivery of many small parts versus the LL-DASH chunked transfer of a single, larger segment over the same time period.These two philosophies demand different things from a CDN. LL-DASH requires the CDN to intelligently cache and serve partially complete files. LL-HLS requires the CDN to handle a massive volume of short, bursty requests and hold connections open for manifest updates. A traditional CDN is optimized for neither.Forging a unified strategyWith two different delivery models, where do you start? You find the one thing they both depend on: the keyframe.Playback can only start from a keyframe (or I-frame). Therefore, the placement of keyframes, which defines the Group of Pictures (GOP), is the foundational layer that both protocols must respect. By enforcing a consistent keyframe interval on the source stream, we could create a predictable media timeline. This timeline can then be described in two different “languages” in the manifests for LL-HLS and LL-DASH.A single timeline with consistent GOPs being packaged for both protocols.This realization led us to a baseline configuration, but each parameter involved a critical engineering trade-off:GOP: 1 second. We chose a frequent, 1-second GOP. The primary benefit is extremely fast stream acquisition; a player never has to wait more than a second for a keyframe to begin playback. The trade-off is a higher bitrate. A 1-second GOP can increase bitrate by 10–15% compared to a more standard 2-second GOP because you're storing more full-frame data. For real-time, interactive use cases, we prioritized startup speed over bitrate savings.Segment Size: 2 seconds. A 2-second segment duration provides a sweet spot. For LL-DASH and modern HLS players, it's short enough to keep manifest sizes manageable. For older, standard HLS clients, it prevents them from falling too far behind the live edge, keeping latency reduced even on legacy players.Part Size: 0.5 seconds. For LL-HLS, this means we deliver four updates per segment. This frequency is aggressive enough to achieve sub-3-second latency while being coarse enough to avoid overwhelming networks with excessive request overhead, which can happen with part durations in the 100–200ms range.Cascading challenges through the pipeline1. Ingest: predictability is paramountTo produce a clean, synchronized output, you need a clean, predictable input. We found that the encoder settings of the source stream are critical. An unstable source with a variable bitrate or erratic keyframe placement will wreck any attempt at low-latency delivery.For our users, we recommend settings that prioritize speed and predictability over compression efficiency:Rate control: Constant Bitrate (CBR)Keyframe interval: A fixed interval (e.g., every 30 frames for 30 FPS, to match our 1s GOP).Encoder tune: zerolatencyAdvanced options: Disable B-frames (bframes=0) and scene-cut detection (scenecut=0) to ensure keyframes are placed exactly where you command them to be.Here is an example ffmpeg command in Bash that encapsulates these principles:ffmpeg -re -i "source.mp4" -c:a aac -c:v libx264 \ -profile:v baseline -tune zerolatency -preset veryfast \ -x264opts "bframes=0:scenecut=0:keyint=30" \ -f flv "rtmp://your-ingest-url"2. Transcoding and packagingOur transcoding and Just-In-Time Packaging (JITP) layer is where the unification truly happens. This component does more than just convert codecs; it has to operate on a stream that is fundamentally incomplete.The primary challenge is that the packager must generate manifests and parts from media files that are still being written by the transcoder. This requires a tightly-coupled architecture where the packager can safely read from the transcoder's buffer.To handle the unpredictable nature of live sources, especially user-generated content via WebRTC, we use a hybrid workflow:GPU Workers (Nvidia/Intel): These handle the heavy lifting of decoding and encoding. Offloading to GPU hardware is crucial for minimizing processing latency and preserving advanced color formats like HDR+.Software Workers and Filters: These provide flexibility. When a live stream from a mobile device suddenly changes resolution or its framerate drops due to a poor connection, a rigid hardware pipeline would crash. Our software layer can handle these context changes gracefully, for instance, by scaling the erratic source and overlaying it on a stable, black-background canvas, meaning the output stream never stops.This makes our JITP a universal packager, creating three synchronized content types from a single, resilient source:LL-DASH (CMAF)LL-HLS (CMAF)Standard HLS (MPEG-TS) for backward compatibility3. CDN delivery: solving two problems at onceThis was the most intensive part of the engineering effort. Our CDN had to be taught how to excel at two completely different, high-performance tasks simultaneously.For LL-DASH, we developed a custom caching module we call chunked-proxy. When the first request for a new .m4s segment arrives, our edge server requests it from the origin. As bytes flow in from the origin, the chunked-proxy immediately forwards them to the client. When a second client requests the same file, our edge server serves all the bytes it has already cached and then appends the new bytes to both clients' streams simultaneously. It’s a multi-client cache for in-flight data.For LL-HLS, the challenges were different:Handling Blocked Requests: Our edge servers needed to be optimized to hold thousands of manifest requests open for hundreds of milliseconds without consuming excessive resources.Intelligent Caching: We needed to correctly handle cache statuses (MISS, EXPIRED) for manifests to ensure only one request goes to the origin per update, preventing a "thundering herd" problem.High Request Volume: LL-HLS generates a storm of requests for tiny part-files. Our infrastructure was scaled and optimized to serve these small files with minimal overhead.The payoff: ultimate flexibility for developersThis engineering effort wasn't just an academic exercise. It provides tangible benefits to developers building with our platform. The primary benefit is simplicity through unification, but the most powerful benefit is the ability to optimize for every platform.Consider the complex landscape of Apple devices. With our unified pipeline, you can create a player logic that does this:On iOS 17.1+: Use LL-DASH with the new Managed Media Source (MMS) API for ~2.0 second latency.On iOS 14.0 - 17.0: Use native LL-HLS for ~3.0 second latency.On older iOS versions: Automatically fall back to standard HLS with a reduced latency of ~9 seconds.This lets you provide the best possible experience on every device, all from a single backend and a single live source, without any extra configuration.Don't fly blind: observability in a low-latency worldA complex system is useless without visibility, and traditional metrics can be misleading for low-latency streaming. Simply looking at response_time from a CDN log is not enough.We had to rethink what to measure. For example:For an LL-HLS manifest, a high response_time (e.g., 500ms) is expected behavior, as it reflects the server correctly holding the request while waiting for the next part. A low response_time could actually indicate a problem. We monitor “Manifest Hold Time” to ensure this blocking mechanism is working as intended.For LL-DASH, a player requesting a chunk that isn't ready yet might receive a 404 Not Found error. While occasional 404s are normal, a spike can indicate origin-to-edge latency issues. This metric, combined with monitoring player liveCatchup behavior, gives a true picture of stream health.Gcore: one pipeline to serve them allThe paths of LL-HLS and LL-DASH may be different, but their destination is the same: real-time interaction with a global audience. By starting with a common foundation—the keyframe—and custom-engineering every component of our pipeline to handle this duality, we successfully solved the unification problem.The result is a single, robust system that gives developers the power of both protocols without the complexity of running two separate infrastructures. It’s how we deliver ±2.0s latency with LL-DASH and ±3.0s with LL-HLS, and it’s the foundation upon which we’ll build to push the boundaries of real-time streaming even further.

Gcore CDN updates: Dedicated IP and BYOIP now available

We’re pleased to announce two new premium features for Gcore CDN: Dedicated IP and Bring Your Own IP (BYOIP). These capabilities give customers more control over their CDN configuration, helping you meet strict security, compliance, and branding requirements.Many organizations, especially in finance and other regulated sectors, require full control over their network identity. With these new features, Gcore enables customers to use unique, dedicated IP addresses to meet compliance or security standards; retain ownership and visibility over IP reputation and routing, and deliver content globally while maintaining trusted, verifiable IP associations.Read on for more information about the benefits of both updates.Dedicated IP: exclusive addresses for your CDN resourcesThe Dedicated IP feature enables customers to assign a private IP address to their CDN configuration, rather than using shared ones. This is ideal for:Businesses that are subject to strict security or legal frameworks.Customers who want isolated IP resources to ensure consistent access and reputation.Teams using WAAP or other advanced security solutions where dedicated IPs simplify policy management.BYOIP: bring your own IP range to Gcore CDNWith Bring Your Own IP (BYOIP), customers can use their own public IP address range while leveraging the performance and global reach of Gcore CDN. This option is especially useful for:Resellers who prefer to keep Gcore infrastructure invisible to end clients.Enterprises maintaining brand consistency and control over IP reputation.How to get startedBoth features are currently available as paid add-ons and are configured manually by the Gcore team. To request activation or learn more, please contact Gcore Support or your account manager.We’re working on making these features easier to manage and automate in future releases. As always, we welcome your feedback on both the feature functionality and the request process—your insights help us improve the Gcore CDN experience for everyone.Get in touch for more information

Smart caching and predictive streaming: the next generation of content delivery

As streaming demand surges worldwide, providers face mounting pressure to deliver high-quality video without buffering, lag, or quality dips, no matter where the viewer is or what device they're using. That pressure is only growing as audiences consume content across mobile, desktop, smart TVs, and edge-connected devices.Traditional content delivery networks (CDNs) were built to handle scale, but not prediction. They reacted to demand, but couldn’t anticipate it. That’s changing.Today, predictive streaming and AI-powered smart caching are enabling a proactive, intelligent approach to content delivery. These technologies go beyond delivering content by forecasting what users will need and making sure it's there before it's even requested. For network engineers, platform teams, and content providers, this marks a major evolution in performance, reliability, and cost control.What are predictive streaming and smart caching?Predictive streaming is a technology that uses AI to anticipate what a viewer will watch next, so the content can be ready before it's requested. That might mean preloading the next episode in a series, caching popular highlights from a live event, or delivering region-specific content based on localized viewing trends.Smart caching supports this by storing that predicted content on servers closer to the viewer, reducing delays and buffering. Together, they make streaming faster and smoother by preparing content in advance based on user behavior.Unlike traditional caching, which relies on static popularity metrics or simple geolocation, predictive streaming is dynamic. It adapts in real time to what’s happening on the platform: user actions, traffic spikes, network conditions, and content trends. This results in:Faster playback with minimal bufferingReduced bandwidth and server loadHigher quality of experience (QoE) scores across user segmentsFor example, during the 2024 UEFA European Championship, several broadcasters used predictive caching to preload high-traffic game segments and highlight reels based on past viewer drop-off points. This allowed for instant replay delivery in multiple languages without overloading central servers.Why predictive streaming matters for viewersGlobally, viewers tend to binge-watch new streaming platform releases. For example, sci-fi-action drama Fallout got 25% of its annual US viewing minutes (2.9 billion minutes) in its first few days of release. The South Korean series Queen of Tears became Netflix's most-watched Korean drama of all time in 2024, amassing over 682.6 million hours viewed globally, with more than half of those watch hours occurring during its six-week broadcast run.A predictive caching system can take advantage of this launch-day momentum by pre-positioning likely-to-be-watched episodes, trailers, or bonus content at the edge, customized by region, device, or time of day.The result is a seamless, high-performance experience that anticipates user behavior and scales intelligently to meet it.Benefits for streaming providersTraditional CDNs often waste resources caching content that may never be viewed. Predictive caching focuses only on content that is likely to be accessed, leading to:Lower egress costsReduced server loadMore efficient cache hit ratiosOne of the core benefits of predictive streaming is latency reduction. By caching content at the edge before it’s requested, platforms avoid the delay caused by round-trips to origin servers. This is especially critical for:Live sports and eventsInteractive or real-time formats (e.g., polls, chats, synchronized streams)Edge environments with unreliable last-mile connectivityFor instance, during the 2024 Copa América, mobile viewers in remote areas of Argentina were able to stream matches without delay thanks to proactive edge caching based on geo-temporal viewing predictions.How it worksAt the core of predictive streaming is smart caching: the process of storing data closer to the end user before it’s explicitly requested. Here’s how it works:Data ingestion: The system gathers data on user behavior, device types, content popularity, and location-based trends.Behavior modeling: AI models identify patterns (e.g., binge-watching behaviors, peak-hour traffic, or regional content spikes).Pre-positioning: Based on predictions, the system caches video segments, trailers, or interactive assets to edge servers closest to where demand is expected.Real-time adaptation: As user behavior changes, the system continuously updates its caching strategy.Use cases across streaming ecosystemsSmart caching and predictive delivery benefit nearly every vertical of streaming.Esports and gaming platforms: Live tournaments generate unpredictable traffic surges, especially when underdog teams advance. Predictive caching helps preload high-interest match content, post-game analysis, and multilingual commentary before traffic spikes hit. This helps provide global availability with minimal delay.Corporate webcasts and investor events: Virtual AGMs or earnings calls need to stream seamlessly to thousands of stakeholders, often under compliance pressure. Predictive systems can cache frequently accessed segments, like executive speeches or financial summaries, at regional nodes.Education platforms: In EdTech environments, predictive delivery ensures that recorded lectures, supplemental materials, and quizzes are ready for users based on their course progression. This reduces lag for remote learners on mobile connections.VOD platforms with regional licensing: Content availability differs across geographies. Predictive caching allows platforms to cache licensed material efficiently and avoid serving geo-blocked content by mistake, while also meeting local performance expectations.Government or emergency broadcasts: During public health updates or crisis communications, predictive streaming can support multi-language delivery, instant replay, and mobile-first optimization without overloading networks during peak alerts.Looking forward: Personalization and platform governanceWe predict that the next wave of predictive streaming will likely include innovations that help platforms scale faster while protecting performance and compliance:Viewer-personalized caching, where individual user profiles guide what’s cached locally (e.g., continuing series, genre preferences)Programmatic cache governance, giving DevOps and marketing teams finer control over how and when content is distributedCross-platform intelligence, allowing syndicated content across services to benefit from shared predictions and joint caching strategiesGcore’s role in the predictive futureAt Gcore, we’re building AI-powered delivery infrastructure that makes the future of streaming a practical reality. Our smart caching, real-time analytics, and global edge network work together to help reduce latency and cost, optimize resource usage, and improve user retention and stream stability.If you’re ready to unlock the next level of content delivery, Gcore’s team is here to help you assess your current setup and plan your predictive evolution.Discover how Gcore streaming technologies helped fan.at boost subscription revenue by 133%

Protecting networks at scale with AI security strategies

Network cyberattacks are no longer isolated incidents. They are a constant, relentless assault on network infrastructure, probing for vulnerabilities in routing, session handling, and authentication flows. With AI at their disposal, threat actors can move faster than ever, shifting tactics mid-attack to bypass static defenses.Legacy systems, designed for simpler threats, cannot keep pace. Modern network security demands a new approach, combining real-time visibility, automated response, AI-driven adaptation, and decentralized protection to secure critical infrastructure without sacrificing speed or availability.At Gcore, we believe security must move as fast as your network does. So, in this article, we explore how L3/L4 network security is evolving to meet new network security challenges and how AI strengthens defenses against today’s most advanced threats.Smarter threat detection across complex network layersModern threats blend into legitimate traffic, using encrypted command-and-control, slow drip API abuse, and DNS tunneling to evade detection. Attackers increasingly embed credential stuffing into regular login activity. Without deep flow analysis, these attempts bypass simple rate limits and avoid triggering alerts until major breaches occur.Effective network defense today means inspection at Layer 3 and Layer 4, looking at:Traffic flow metadata (NetFlow, sFlow)SSL/TLS handshake anomaliesDNS request irregularitiesUnexpected session persistence behaviorsGcore Edge Security applies real-time traffic inspection across multiple layers, correlating flows and behaviors across routers, load balancers, proxies, and cloud edges. Even slight anomalies in NetFlow exports or unexpected east-west traffic inside a VPC can trigger early threat alerts.By combining packet metadata analysis, flow telemetry, and historical modeling, Gcore helps organizations detect stealth attacks long before traditional security controls react.Automated response to contain threats at network speedDetection is only half the battle. Once an anomaly is identified, defenders must act within seconds to prevent damage.Real-world example: DNS amplification attackIf a volumetric DNS amplification attack begins saturating a branch office's upstream link, automated systems can:Apply ACL-based rate limits at the nearest edge routerFilter malicious traffic upstream before WAN degradationAlert teams for manual inspection if thresholds escalateSimilarly, if lateral movement is detected inside a cloud deployment, dynamic firewall policies can isolate affected subnets before attackers pivot deeper.Gcore’s network automation frameworks integrate real-time AI decision-making with response workflows, enabling selective throttling, forced reauthentication, or local isolation—without disrupting legitimate users. Automation means threats are contained quickly, minimizing impact without crippling operations.Hardening DDoS mitigation against evolving attack patternsDDoS attacks have moved beyond basic volumetric floods. Today, attackers combine multiple tactics in coordinated strikes. Common attack vectors in modern DDoS include the following:UDP floods targeting bandwidth exhaustionSSL handshake floods overwhelming load balancersHTTP floods simulating legitimate browser sessionsAdaptive multi-vector shifts changing methods mid-attackReal-world case study: ISP under hybrid DDoS attackIn recent years, ISPs and large enterprises have faced hybrid DDoS attacks blending hundreds of gigabits per second of L3/4 UDP flood traffic with targeted SSL handshake floods. Attackers shift vectors dynamically to bypass static defenses and overwhelm infrastructure at multiple layers simultaneously. Static defenses fail in such cases because attackers change vectors every few minutes.Building resilient networks through self-healing capabilitiesEven the best defenses can be breached. When that happens, resilient networks must recover automatically to maintain uptime.If BGP route flapping is detected on a peering session, self-healing networks can:Suppress unstable prefixesReroute traffic through backup transit providersPrevent packet loss and service degradation without manual interventionSimilarly, if a VPN concentrator faces resource exhaustion from targeted attack traffic, automated scaling can:Spin up additional concentratorsRedistribute tunnel sessions dynamicallyMaintain stable access for remote usersGcore’s infrastructure supports self-healing capabilities by combining telemetry analysis, automated failover, and rapid resource scaling across core and edge networks. This resilience prevents localized incidents from escalating into major outages.Securing the edge against decentralized threatsThe network perimeter is now everywhere. Branches, mobile endpoints, IoT devices, and multi-cloud services all represent potential entry points for attackers.Real-world example: IoT malware infection at the branchMalware-infected IoT devices at a branch office can initiate outbound C2 traffic during low-traffic periods. Without local inspection, this activity can go undetected until aggregated telemetry reaches the central SOC, often too late.Modern edge security platforms deploy the following:Real-time traffic inspection at branch and edge routersBehavioral anomaly detection at local points of presenceAutomated enforcement policies blocking malicious flows immediatelyGcore’s edge nodes analyze flows and detect anomalies in near real time, enabling local containment before threats can propagate deeper into cloud or core systems. Decentralized defense shortens attacker dwell time, minimizes potential damage, and offloads pressure from centralized systems.How Gcore is preparing networks for the next generation of threatsThe threat landscape will only grow more complex. Attackers are investing in automation, AI, and adaptive tactics to stay one step ahead. Defending modern networks demands:Full-stack visibility from core to edgeAdaptive defense that adjusts faster than attackersAutomated recovery from disruption or compromiseDecentralized detection and containment at every entry pointGcore Edge Security delivers these capabilities, combining AI-enhanced traffic analysis, real-time mitigation, resilient failover systems, and edge-to-core defense. In a world where minutes of network downtime can cost millions, you can’t afford static defenses. We enable networks to protect critical infrastructure without sacrificing performance, agility, or resilience.Move faster than attackers. Build AI-powered resilience into your network with Gcore.Check out our docs to see how DDoS Protection protects your network

Introducing Gcore for Startups: created for builders, by builders

Building a startup is tough. Every decision about your infrastructure can make or break your speed to market and burn rate. Your time, team, and budget are stretched thin. That’s why you need a partner that helps you scale without compromise.At Gcore, we get it. We’ve been there ourselves, and we’ve helped thousands of engineering teams scale global applications under pressure.That’s why we created the Gcore Startups Program: to give early-stage founders the infrastructure, support, and pricing they actually need to launch and grow.At Gcore, we launched the Startups Program because we’ve been in their shoes. We know what it means to build under pressure, with limited resources, and big ambitions. We wanted to offer early-stage founders more than just short-term credits and fine print; our goal is to give them robust, long-term infrastructure they can rely on.Dmitry Maslennikov, Head of Gcore for StartupsWhat you get when you joinThe program is open to startups across industries, whether you’re building in fintech, AI, gaming, media, or something entirely new.Here’s what founders receive:Startup-friendly pricing on Gcore’s cloud and edge servicesCloud credits to help you get started without riskWhite-labeled dashboards to track usage across your team or customersPersonalized onboarding and migration supportGo-to-market resources to accelerate your launchYou also get direct access to all Gcore products, including Everywhere Inference, GPU Cloud, Managed Kubernetes, Object Storage, CDN, and security services. They’re available globally via our single, intuitive Gcore Customer Portal, and ready for your production workloads.When startups join the program, they get access to powerful cloud and edge infrastructure at startup-friendly pricing, personal migration support, white-labeled dashboards for tracking usage, and go-to-market resources. Everything we provide is tailored to the specific startup’s unique needs and designed to help them scale faster and smarter.Dmitry MaslennikovWhy startups are choosing GcoreWe understand that performance and flexibility are key for startups. From high-throughput AI inference to real-time media delivery, our infrastructure was designed to support demanding, distributed applications at scale.But what sets us apart is how we work with founders. We don’t force startups into rigid plans or abstract SLAs. We build with you 24/7, because we know your hustle isn’t a 9–5.One recent success story: an AI startup that migrated from a major hyperscaler told us they cut their inference costs by over 40%…and got actual human support for the first time. What truly sets us apart is our flexibility: we’re not a faceless hyperscaler. We tailor offers, support, and infrastructure to each startup’s stage and needs.Dmitry MaslennikovWe’re excited to support startups working on AI, machine learning, video, gaming, and real-time apps. Gcore for Startups is delivering serious value to founders in industries where performance, cost efficiency, and responsiveness make or break product experience.Ready to scale smarter?Apply today and get hands-on support from engineers who’ve been in your shoes. If you’re an early-stage startup with a working product and funding (pre-seed to Series A), we’ll review your application quickly and tailor infrastructure that matches your stage, stack, and goals.To get started, head on over to our Gcore for Startups page and book a demo.Discover Gcore for Startups

Subscribe to our newsletter

Get the latest industry trends, exclusive insights, and Gcore updates delivered straight to your inbox.