Skip to main content

Viewing Post Content

Post content viewing involves understanding the parent-child relationship structure and implementing efficient content rendering for different media types. This guide covers how to access, process, and display various types of post content with optimal performance.
Posts with media attachments (images, files, videos) follow a Parent-Child relationship where the text content is the parent post and each media item is a separate child post. This structure enables flexible content management and efficient media handling.

Architecture Overview

Post Structure & Hierarchy

Understanding the parent-child relationship is crucial for proper content rendering and efficient data access.
  • iOS
  • Android
  • TypeScript
  • Flutter
// Access parent post and its children
func processPostContent(post: AmityPost) {
    // Parent post contains text content and metadata
    if let textData = post.data as? AmityTextPostData {
        print("Parent post text: \(textData.text)")
    }
    
    // Process child posts (media attachments)
    processChildPosts(post.children)
}

func processChildPosts(_ children: [AmityPost]) {
    for childPost in children {
        switch childPost.dataType {
        case .image:
            processImagePost(childPost)
        case .file:
            processFilePost(childPost)
        case .video:
            processVideoPost(childPost)
        case .liveStream:
            processLiveStreamPost(childPost)
        case .poll:
            processPollPost(childPost)
        default:
            print("Unknown post type: \(childPost.dataType)")
        }
    }
}

// Advanced: Hierarchical post processor
class PostContentProcessor {
    func processPost(_ post: AmityPost) -> ProcessedPostContent {
        let parentContent = extractParentContent(post)
        let childContents = post.children.compactMap { processChildPost($0) }
        
        return ProcessedPostContent(
            parent: parentContent,
            children: childContents,
            totalSize: calculateTotalSize(post),
            hasMediaContent: !childContents.isEmpty
        )
    }
    
    private func extractParentContent(_ post: AmityPost) -> ParentContent {
        guard let textData = post.data as? AmityTextPostData else {
            return ParentContent(text: nil, metadata: post.metadata)
        }
        
        return ParentContent(
            text: textData.text,
            metadata: post.metadata,
            mentions: textData.mentions,
            tags: post.tags
        )
    }
}

Image Post Content

Image posts support automatic multi-size generation for optimal performance across different display contexts. Each uploaded image is processed into four different sizes with a maximum file size of 1GB.

Available Image Sizes

SizeDescriptionUse Case
smallThumbnail sizeProfile pictures, preview thumbnails
mediumStandard displayFeed previews, card layouts
largeHigh qualityDetailed view, modal displays
fullOriginal resolutionFull-screen viewing, downloads
  • iOS
  • Android
  • TypeScript
  • Flutter
// Basic image post processing
func processImagePost(_ post: AmityPost) {
    guard let imageData = post.data as? AmityImagePostData else { return }
    
    // Get image information
    let imageInfo = imageData.getImageInfo()
    print("Image size: \(imageInfo.fileSize) bytes")
    print("Dimensions: \(imageInfo.width)x\(imageInfo.height)")
    print("MIME type: \(imageInfo.mimeType)")
    
    // Load different image sizes
    loadImageWithSize(imageData, size: .medium)
}

// Advanced: Optimized image loading with caching
class ImagePostManager {
    private let fileRepository: AmityFileRepository
    private let imageCache = NSCache<NSString, UIImage>()
    
    init(client: AmityClient) {
        self.fileRepository = AmityFileRepository(client: client)
    }
    
    func loadOptimalImage(
        from imageData: AmityImagePostData,
        for displaySize: CGSize,
        completion: @escaping (UIImage?) -> Void
    ) {
        let optimalSize = determineOptimalSize(for: displaySize)
        let cacheKey = "\(imageData.fileId)_\(optimalSize.rawValue)" as NSString
        
        // Check cache first
        if let cachedImage = imageCache.object(forKey: cacheKey) {
            completion(cachedImage)
            return
        }
        
        // Download and cache image
        fileRepository.downloadImage(
            imageData.getImageInfo(),
            size: optimalSize
        ) { [weak self] result in
            switch result {
            case .success(let image):
                self?.imageCache.setObject(image, forKey: cacheKey)
                completion(image)
            case .failure(let error):
                print("Failed to load image: \(error)")
                completion(nil)
            }
        }
    }
    
    private func determineOptimalSize(for displaySize: CGSize) -> AmityImageSize {
        let maxDimension = max(displaySize.width, displaySize.height)
        
        switch maxDimension {
        case 0...150:
            return .small
        case 151...400:
            return .medium
        case 401...800:
            return .large
        default:
            return .full
        }
    }
    
    // Manual URL construction for custom implementations
    func constructImageURL(
        baseURL: String,
        size: AmityImageSize
    ) -> String {
        return "\(baseURL)?size=\(size.rawValue)"
    }
}

// Progressive image loading
func loadImageProgressively(
    imageData: AmityImagePostData,
    imageView: UIImageView
) {
    // Start with small size for quick preview
    loadImageWithSize(imageData, size: .small) { smallImage in
        DispatchQueue.main.async {
            imageView.image = smallImage
        }
        
        // Load higher quality version
        self.loadImageWithSize(imageData, size: .large) { largeImage in
            DispatchQueue.main.async {
                UIView.transition(with: imageView, duration: 0.3, options: .transitionCrossDissolve) {
                    imageView.image = largeImage
                }
            }
        }
    }
}

URL Construction for Custom Implementations

For custom image loading implementations, you can construct image URLs manually by appending the size query parameter:
// Base URL
https://my-image-download-link-from-amity/123/456

// With size parameter
https://my-image-download-link-from-amity/123/456?size=medium
Valid size parameters: small, medium, large, full

File Post Content

File posts support various document types including PDFs, Word documents, spreadsheets, and more. Each file attachment maintains its original metadata and provides secure download capabilities with a maximum file size of 1GB.

Supported File Types

Documents

PDF, DOC, DOCX, TXT, RTF

Spreadsheets

XLS, XLSX, CSV, ODS

Presentations

PPT, PPTX, ODP

Archives

ZIP, RAR, 7Z, TAR

Code Files

JS, TS, PY, JAVA, CPP

Others

Any file type up to 1GB
  • iOS
  • Android
  • TypeScript
  • Flutter
// Basic file post processing
func processFilePost(_ post: AmityPost) {
    guard let fileData = post.data as? AmityFilePostData else { return }
    
    // Get file information
    let fileInfo = fileData.getFileInfo()
    print("File name: \(fileInfo.fileName)")
    print("File size: \(fileInfo.fileSize) bytes")
    print("MIME type: \(fileInfo.mimeType)")
    print("File extension: \(fileInfo.fileExtension)")
    
    // Download file
    downloadFile(fileData: fileData)
}

// Advanced: File manager with download progress
class FilePostManager {
    private let fileRepository: AmityFileRepository
    private var downloadTasks: [String: URLSessionDownloadTask] = [:]
    
    init(client: AmityClient) {
        self.fileRepository = AmityFileRepository(client: client)
    }
    
    func downloadFile(
        fileData: AmityFilePostData,
        progressHandler: @escaping (Double) -> Void,
        completion: @escaping (Result<URL, Error>) -> Void
    ) {
        let fileInfo = fileData.getFileInfo()
        let taskId = fileInfo.fileId
        
        // Cancel existing download if any
        downloadTasks[taskId]?.cancel()
        
        fileRepository.downloadFile(fileInfo) { progress in
            DispatchQueue.main.async {
                progressHandler(progress)
            }
        } completion: { [weak self] result in
            DispatchQueue.main.async {
                self?.downloadTasks.removeValue(forKey: taskId)
                
                switch result {
                case .success(let localURL):
                    completion(.success(localURL))
                case .failure(let error):
                    completion(.failure(error))
                }
            }
        }
    }
    
    func cancelDownload(fileId: String) {
        downloadTasks[fileId]?.cancel()
        downloadTasks.removeValue(forKey: fileId)
    }
    
    // Get file icon based on type
    func getFileIcon(for mimeType: String) -> UIImage? {
        switch mimeType {
        case let type where type.hasPrefix("application/pdf"):
            return UIImage(systemName: "doc.fill")
        case let type where type.hasPrefix("application/msword"),
             let type where type.contains("wordprocessingml"):
            return UIImage(systemName: "doc.text.fill")
        case let type where type.contains("spreadsheetml"),
             let type where type.hasPrefix("application/vnd.ms-excel"):
            return UIImage(systemName: "tablecells.fill")
        case let type where type.contains("presentationml"),
             let type where type.hasPrefix("application/vnd.ms-powerpoint"):
            return UIImage(systemName: "rectangle.fill.on.rectangle.fill")
        case let type where type.hasPrefix("application/zip"),
             let type where type.hasPrefix("application/x-rar"):
            return UIImage(systemName: "archivebox.fill")
        case let type where type.hasPrefix("image/"):
            return UIImage(systemName: "photo.fill")
        default:
            return UIImage(systemName: "doc.fill")
        }
    }
    
    // Format file size for display
    func formatFileSize(_ bytes: Int64) -> String {
        let formatter = ByteCountFormatter()
        formatter.countStyle = .file
        formatter.allowedUnits = [.useKB, .useMB, .useGB]
        return formatter.string(fromByteCount: bytes)
    }
}

// Custom file view controller
class FileViewController: UIViewController {
    @IBOutlet weak var fileIconImageView: UIImageView!
    @IBOutlet weak var fileNameLabel: UILabel!
    @IBOutlet weak var fileSizeLabel: UILabel!
    @IBOutlet weak var downloadButton: UIButton!
    @IBOutlet weak var progressView: UIProgressView!
    
    private let fileManager = FilePostManager(client: AmityClient.shared)
    private var fileData: AmityFilePostData!
    
    func configure(with fileData: AmityFilePostData) {
        self.fileData = fileData
        setupUI()
    }
    
    private func setupUI() {
        let fileInfo = fileData.getFileInfo()
        
        fileNameLabel.text = fileInfo.fileName
        fileSizeLabel.text = fileManager.formatFileSize(fileInfo.fileSize)
        fileIconImageView.image = fileManager.getFileIcon(for: fileInfo.mimeType)
        
        progressView.isHidden = true
        downloadButton.setTitle("Download", for: .normal)
    }
    
    @IBAction func downloadButtonTapped(_ sender: UIButton) {
        downloadButton.isEnabled = false
        progressView.isHidden = false
        progressView.progress = 0.0
        
        fileManager.downloadFile(
            fileData: fileData,
            progressHandler: { [weak self] progress in
                self?.progressView.progress = Float(progress)
            },
            completion: { [weak self] result in
                self?.downloadButton.isEnabled = true
                self?.progressView.isHidden = true
                
                switch result {
                case .success(let localURL):
                    self?.openFile(at: localURL)
                case .failure(let error):
                    self?.showError(error)
                }
            }
        )
    }
    
    private func openFile(at url: URL) {
        let documentController = UIDocumentInteractionController(url: url)
        documentController.presentPreview(animated: true)
    }
    
    private func showError(_ error: Error) {
        let alert = UIAlertController(
            title: "Download Failed",
            message: error.localizedDescription,
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        present(alert, animated: true)
    }
}

Best Practices for File Posts

  • Always validate file types and sizes before processing
  • Implement virus scanning for uploaded files
  • Use secure download URLs with expiration
  • Check file permissions before download attempts
  • Implement download progress indicators for better UX
  • Cache file metadata to avoid repeated API calls
  • Use background downloads for large files
  • Implement download queue management for multiple files
  • Handle network interruptions gracefully
  • Provide retry mechanisms for failed downloads
  • Show meaningful error messages to users
  • Implement fallback mechanisms for unsupported file types
  • Clean up temporary files after use
  • Implement storage quota management
  • Provide options to delete downloaded files
  • Monitor available storage space
For detailed file handling information, refer to the File Management Guide.

Video Post Content

Video posts support automatic transcoding into multiple resolutions for optimal playback across different devices and network conditions. Each uploaded video (up to 1GB) is processed into various quality levels while maintaining the original version for high-quality viewing.

Available Video Resolutions

1080p (Full HD)

High quality for desktop and large screens

720p (HD)

Standard quality for most devices

480p (SD)

Medium quality for mobile and slower connections

360p (Low)

Low quality for very slow connections

Original

Original uploaded resolution (immediately available)
Transcoding Process: While the original video is immediately available after upload, transcoded versions take time to process. Always check resolution availability before playback.
  • iOS
  • Android
  • TypeScript
  • Flutter
// Basic video post processing
func processVideoPost(_ post: AmityPost) {
    guard let videoData = post.data as? AmityVideoPostData else { return }
    
    // Get video information
    let videoInfo = videoData.getVideoInfo()
    print("Video duration: \(videoInfo.duration) seconds")
    print("Video size: \(videoInfo.fileSize) bytes")
    print("Original resolution: \(videoInfo.width)x\(videoInfo.height)")
    
    // Get available video qualities
    let availableQualities = videoData.getVideoStreams()
    for quality in availableQualities {
        print("Quality: \(quality.resolution) - Ready: \(quality.isReady)")
    }
    
    // Play video with optimal quality
    playVideoWithOptimalQuality(videoData: videoData)
}

// Advanced: Video player manager with adaptive streaming
class VideoPostManager {
    private var player: AVPlayer?
    private var playerLayer: AVPlayerLayer?
    private let networkMonitor = NWPathMonitor()
    
    func setupVideoPlayer(
        for videoData: AmityVideoPostData,
        in containerView: UIView
    ) {
        let videoURL = getOptimalVideoURL(videoData: videoData)
        
        // Create player
        player = AVPlayer(url: videoURL)
        playerLayer = AVPlayerLayer(player: player)
        playerLayer?.frame = containerView.bounds
        playerLayer?.videoGravity = .resizeAspectFit
        
        if let playerLayer = playerLayer {
            containerView.layer.addSublayer(playerLayer)
        }
        
        // Monitor network changes for adaptive streaming
        setupNetworkMonitoring(videoData: videoData)
        
        // Add playback observers
        setupPlaybackObservers()
    }
    
    private func getOptimalVideoURL(videoData: AmityVideoPostData) -> URL {
        let availableStreams = videoData.getVideoStreams()
        let networkSpeed = getCurrentNetworkSpeed()
        
        // Select quality based on network conditions
        let optimalStream: AmityVideoStream
        switch networkSpeed {
        case .high:
            optimalStream = availableStreams.first { $0.resolution == "1080p" && $0.isReady } ??
                           availableStreams.first { $0.resolution == "720p" && $0.isReady } ??
                           availableStreams.first { $0.resolution == "original" }!
        case .medium:
            optimalStream = availableStreams.first { $0.resolution == "720p" && $0.isReady } ??
                           availableStreams.first { $0.resolution == "480p" && $0.isReady } ??
                           availableStreams.first { $0.resolution == "original" }!
        case .low:
            optimalStream = availableStreams.first { $0.resolution == "360p" && $0.isReady } ??
                           availableStreams.first { $0.resolution == "480p" && $0.isReady } ??
                           availableStreams.first { $0.resolution == "original" }!
        }
        
        return URL(string: optimalStream.url)!
    }
    
    private func setupNetworkMonitoring(videoData: AmityVideoPostData) {
        networkMonitor.pathUpdateHandler = { [weak self] path in
            DispatchQueue.main.async {
                // Adapt video quality based on network changes
                if path.status == .satisfied {
                    self?.adaptVideoQuality(videoData: videoData, networkPath: path)
                }
            }
        }
        
        let queue = DispatchQueue(label: "NetworkMonitor")
        networkMonitor.start(queue: queue)
    }
    
    private func adaptVideoQuality(videoData: AmityVideoPostData, networkPath: NWPath) {
        guard let currentItem = player?.currentItem else { return }
        
        let currentTime = currentItem.currentTime()
        let newURL = getOptimalVideoURL(videoData: videoData)
        
        // Seamlessly switch video quality
        let newPlayerItem = AVPlayerItem(url: newURL)
        player?.replaceCurrentItem(with: newPlayerItem)
        player?.seek(to: currentTime)
    }
    
    private func setupPlaybackObservers() {
        // Add observers for playback events
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(playerDidFinishPlaying),
            name: .AVPlayerItemDidPlayToEndTime,
            object: player?.currentItem
        )
        
        // Monitor playback status
        player?.addObserver(
            self,
            forKeyPath: "status",
            options: [.new, .initial],
            context: nil
        )
    }
    
    @objc private func playerDidFinishPlaying() {
        // Handle video completion
        player?.seek(to: .zero)
    }
    
    // Video thumbnail generation
    func generateThumbnail(
        from videoData: AmityVideoPostData,
        at time: CMTime = CMTime(seconds: 1, preferredTimescale: 60),
        completion: @escaping (UIImage?) -> Void
    ) {
        let videoURL = URL(string: videoData.getVideoInfo().url)!
        let asset = AVAsset(url: videoURL)
        let imageGenerator = AVAssetImageGenerator(asset: asset)
        imageGenerator.appliesPreferredTrackTransform = true
        
        imageGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: time)]) { _, cgImage, _, _, _ in
            DispatchQueue.main.async {
                if let cgImage = cgImage {
                    completion(UIImage(cgImage: cgImage))
                } else {
                    completion(nil)
                }
            }
        }
    }
    
    // Playback controls
    func play() {
        player?.play()
    }
    
    func pause() {
        player?.pause()
    }
    
    func seek(to time: CMTime) {
        player?.seek(to: time)
    }
    
    func getCurrentTime() -> CMTime {
        return player?.currentTime() ?? .zero
    }
    
    func getDuration() -> CMTime {
        return player?.currentItem?.duration ?? .zero
    }
    
    deinit {
        networkMonitor.cancel()
        player?.pause()
        playerLayer?.removeFromSuperlayer()
    }
}

// Custom video player view controller
class VideoPlayerViewController: UIViewController {
    @IBOutlet weak var videoContainerView: UIView!
    @IBOutlet weak var playPauseButton: UIButton!
    @IBOutlet weak var progressSlider: UISlider!
    @IBOutlet weak var currentTimeLabel: UILabel!
    @IBOutlet weak var durationLabel: UILabel!
    @IBOutlet weak var qualityButton: UIButton!
    
    private let videoManager = VideoPostManager()
    private var videoData: AmityVideoPostData!
    private var progressTimer: Timer?
    
    func configure(with videoData: AmityVideoPostData) {
        self.videoData = videoData
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupVideoPlayer()
        setupQualityMenu()
        startProgressTimer()
    }
    
    private func setupVideoPlayer() {
        videoManager.setupVideoPlayer(for: videoData, in: videoContainerView)
        
        let duration = videoManager.getDuration()
        durationLabel.text = formatTime(duration)
        progressSlider.maximumValue = Float(CMTimeGetSeconds(duration))
    }
    
    private func setupQualityMenu() {
        let availableQualities = videoData.getVideoStreams()
        let actions = availableQualities.compactMap { stream -> UIAction? in
            guard stream.isReady else { return nil }
            return UIAction(title: stream.resolution.capitalized) { _ in
                // Switch video quality
                self.switchVideoQuality(to: stream)
            }
        }
        
        let menu = UIMenu(title: "Video Quality", children: actions)
        qualityButton.menu = menu
        qualityButton.showsMenuAsPrimaryAction = true
    }
    
    private func switchVideoQuality(to stream: AmityVideoStream) {
        // Implementation for manual quality switching
        let currentTime = videoManager.getCurrentTime()
        let newURL = URL(string: stream.url)!
        
        // Create new player item and maintain playback position
        // Implementation details...
    }
    
    @IBAction func playPauseButtonTapped(_ sender: UIButton) {
        if sender.isSelected {
            videoManager.pause()
            sender.isSelected = false
        } else {
            videoManager.play()
            sender.isSelected = true
        }
    }
    
    @IBAction func progressSliderChanged(_ sender: UISlider) {
        let time = CMTime(seconds: Double(sender.value), preferredTimescale: 60)
        videoManager.seek(to: time)
    }
    
    private func startProgressTimer() {
        progressTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
            self.updateProgress()
        }
    }
    
    private func updateProgress() {
        let currentTime = videoManager.getCurrentTime()
        let seconds = CMTimeGetSeconds(currentTime)
        
        progressSlider.value = Float(seconds)
        currentTimeLabel.text = formatTime(currentTime)
    }
    
    private func formatTime(_ time: CMTime) -> String {
        let seconds = Int(CMTimeGetSeconds(time))
        let mins = seconds / 60
        let secs = seconds % 60
        return String(format: "%02d:%02d", mins, secs)
    }
    
    deinit {
        progressTimer?.invalidate()
    }
}

Video Transcoding Status

Understanding transcoding status is crucial for providing the best user experience:
  • Original Quality: Available immediately after upload
  • Use original quality as fallback while transcoded versions process
  • Monitor transcoding progress for quality upgrades
  • 360p: Usually ready within 1-2 minutes
  • 480p: Ready within 2-3 minutes
  • 720p: Ready within 3-5 minutes
  • 1080p: Ready within 5-10 minutes
  • Processing time varies with video length and complexity
  • Start with the highest available quality for current network
  • Implement fallback chain: preferred → available → original
  • Use adaptive streaming when multiple qualities are ready

Best Practices for Video Posts

Network Optimization

  • Implement adaptive bitrate streaming
  • Monitor network conditions continuously
  • Provide manual quality selection options
  • Cache video metadata for quick access

User Experience

  • Show loading states with thumbnails
  • Provide seek preview thumbnails
  • Implement background/foreground handling
  • Add picture-in-picture support where available

Performance

  • Pre-generate video thumbnails
  • Implement video preloading for better UX
  • Use efficient video codecs (H.264/H.265)
  • Monitor memory usage during playback

Error Handling

  • Handle network interruptions gracefully
  • Provide retry mechanisms for failed streams
  • Show meaningful error messages
  • Implement fallback to lower qualities
For comprehensive video handling information, refer to the Video Handling Guide.

Live Stream Post Content

Live stream posts enable real-time video broadcasting and interactive engagement within social feeds. These posts support live video streaming with real-time chat, viewer interactions, and automatic recording capabilities.

Live Stream Features

Real-time Broadcasting

High-quality live video streaming with adaptive bitrate

Interactive Chat

Real-time messaging and audience engagement

Stream Recording

Automatic recording for post-stream playback

Viewer Analytics

Live viewer count and engagement metrics
  • iOS
  • Android
  • TypeScript
// Basic live stream post processing
func processLiveStreamPost(_ post: AmityPost) {
    guard let streamData = post.data as? AmityLiveStreamPostData else { return }
    
    // Get stream information
    let streamInfo = streamData.getStreamInfo()
    print("Stream ID: \(streamInfo.streamId)")
    print("Stream status: \(streamInfo.status)")
    print("Viewer count: \(streamInfo.viewerCount)")
    print("Stream URL: \(streamInfo.streamUrl)")
    
    // Check if stream is live
    if streamInfo.status == .live {
        startWatchingLiveStream(streamData: streamData)
    } else if streamInfo.status == .ended && streamInfo.recordingUrl != nil {
        playRecordedStream(recordingUrl: streamInfo.recordingUrl!)
    }
}

// Advanced: Live stream manager with WebRTC support
class LiveStreamManager: NSObject {
    private var streamRepository: AmityStreamRepository
    private var webRTCClient: WebRTCClient?
    private var streamView: UIView?
    private var chatManager: StreamChatManager
    private var analytics: StreamAnalytics
    
    init(client: AmityClient) {
        self.streamRepository = AmityStreamRepository(client: client)
        self.chatManager = StreamChatManager(client: client)
        self.analytics = StreamAnalytics()
        super.init()
    }
    
    // Start watching a live stream
    func startWatchingStream(
        streamData: AmityLiveStreamPostData,
        in containerView: UIView,
        completion: @escaping (Result<Void, Error>) -> Void
    ) {
        let streamInfo = streamData.getStreamInfo()
        
        // Verify stream is live
        guard streamInfo.status == .live else {
            completion(.failure(StreamError.streamNotLive))
            return
        }
        
        // Initialize WebRTC client for low-latency streaming
        webRTCClient = WebRTCClient()
        webRTCClient?.delegate = self
        
        // Setup stream view
        streamView = RTCEAGLVideoView(frame: containerView.bounds)
        containerView.addSubview(streamView!)
        
        // Connect to stream
        webRTCClient?.connect(to: streamInfo.streamUrl) { [weak self] result in
            DispatchQueue.main.async {
                switch result {
                case .success:
                    self?.analytics.trackStreamStart(streamId: streamInfo.streamId)
                    completion(.success(()))
                case .failure(let error):
                    completion(.failure(error))
                }
            }
        }
        
        // Start receiving chat messages
        chatManager.startReceivingMessages(for: streamInfo.streamId)
        
        // Update viewer count periodically
        startViewerCountUpdates(streamId: streamInfo.streamId)
    }
    
    // Play recorded stream (for ended streams)
    func playRecordedStream(
        recordingUrl: String,
        in containerView: UIView
    ) {
        let player = AVPlayer(url: URL(string: recordingUrl)!)
        let playerLayer = AVPlayerLayer(player: player)
        playerLayer.frame = containerView.bounds
        playerLayer.videoGravity = .resizeAspectFit
        
        containerView.layer.addSublayer(playerLayer)
        player.play()
        
        analytics.trackRecordingPlay(recordingUrl: recordingUrl)
    }
    
    // Send chat message during live stream
    func sendChatMessage(
        streamId: String,
        message: String,
        completion: @escaping (Result<Void, Error>) -> Void
    ) {
        chatManager.sendMessage(
            streamId: streamId,
            message: message,
            completion: completion
        )
    }
    
    // React to live stream (like, heart, etc.)
    func sendStreamReaction(
        streamId: String,
        reaction: StreamReaction,
        completion: @escaping (Result<Void, Error>) -> Void
    ) {
        streamRepository.sendReaction(
            streamId: streamId,
            reaction: reaction
        ) { result in
            DispatchQueue.main.async {
                completion(result)
            }
        }
    }
    
    private func startViewerCountUpdates(streamId: String) {
        Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] timer in
            self?.streamRepository.getViewerCount(streamId: streamId) { count in
                DispatchQueue.main.async {
                    self?.analytics.updateViewerCount(count)
                    NotificationCenter.default.post(
                        name: .streamViewerCountUpdated,
                        object: count
                    )
                }
            }
        }
    }
    
    // Get stream statistics
    func getStreamStatistics(
        streamId: String,
        completion: @escaping (StreamStatistics?) -> Void
    ) {
        streamRepository.getStreamStatistics(streamId: streamId) { stats in
            DispatchQueue.main.async {
                completion(stats)
            }
        }
    }
    
    // Stop watching stream
    func stopWatching() {
        webRTCClient?.disconnect()
        chatManager.stopReceivingMessages()
        streamView?.removeFromSuperview()
        analytics.trackStreamEnd()
    }
}

// WebRTC delegate for handling stream events
extension LiveStreamManager: WebRTCClientDelegate {
    func webRTCClient(_ client: WebRTCClient, didReceiveRemoteVideoTrack videoTrack: RTCVideoTrack) {
        DispatchQueue.main.async {
            videoTrack.add(self.streamView as! RTCVideoRenderer)
        }
    }
    
    func webRTCClient(_ client: WebRTCClient, didChangeConnectionState state: RTCIceConnectionState) {
        DispatchQueue.main.async {
            switch state {
            case .connected:
                print("Stream connected successfully")
            case .disconnected:
                print("Stream disconnected")
            case .failed:
                print("Stream connection failed")
            default:
                break
            }
        }
    }
}

// Live stream view controller
class LiveStreamViewController: UIViewController {
    @IBOutlet weak var streamContainerView: UIView!
    @IBOutlet weak var chatTableView: UITableView!
    @IBOutlet weak var messageTextField: UITextField!
    @IBOutlet weak var sendButton: UIButton!
    @IBOutlet weak var viewerCountLabel: UILabel!
    @IBOutlet weak var statusLabel: UILabel!
    @IBOutlet weak var reactionButtonsStackView: UIStackView!
    
    private let streamManager = LiveStreamManager(client: AmityClient.shared)
    private var streamData: AmityLiveStreamPostData!
    private var chatMessages: [StreamChatMessage] = []
    
    func configure(with streamData: AmityLiveStreamPostData) {
        self.streamData = streamData
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        startWatchingStream()
        setupChatObservers()
        setupReactionButtons()
    }
    
    private func setupUI() {
        let streamInfo = streamData.getStreamInfo()
        statusLabel.text = streamInfo.status.displayText
        viewerCountLabel.text = "\(streamInfo.viewerCount) viewers"
        
        // Setup chat table view
        chatTableView.delegate = self
        chatTableView.dataSource = self
        
        // Setup message input
        messageTextField.delegate = self
    }
    
    private func startWatchingStream() {
        streamManager.startWatchingStream(
            streamData: streamData,
            in: streamContainerView
        ) { [weak self] result in
            switch result {
            case .success:
                self?.statusLabel.text = "Live"
                self?.statusLabel.textColor = .systemRed
            case .failure(let error):
                self?.showError(error)
            }
        }
    }
    
    private func setupChatObservers() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(newChatMessageReceived(_:)),
            name: .streamChatMessageReceived,
            object: nil
        )
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(viewerCountUpdated(_:)),
            name: .streamViewerCountUpdated,
            object: nil
        )
    }
    
    private func setupReactionButtons() {
        let reactions: [StreamReaction] = [.like, .heart, .clap, .fire]
        
        reactions.forEach { reaction in
            let button = UIButton(type: .system)
            button.setTitle(reaction.emoji, for: .normal)
            button.titleLabel?.font = UIFont.systemFont(ofSize: 24)
            button.addTarget(self, action: #selector(reactionButtonTapped(_:)), for: .touchUpInside)
            button.tag = reaction.rawValue
            reactionButtonsStackView.addArrangedSubview(button)
        }
    }
    
    @objc private func newChatMessageReceived(_ notification: Notification) {
        guard let message = notification.object as? StreamChatMessage else { return }
        
        chatMessages.append(message)
        
        DispatchQueue.main.async {
            let indexPath = IndexPath(row: self.chatMessages.count - 1, section: 0)
            self.chatTableView.insertRows(at: [indexPath], with: .automatic)
            self.chatTableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
        }
    }
    
    @objc private func viewerCountUpdated(_ notification: Notification) {
        guard let count = notification.object as? Int else { return }
        viewerCountLabel.text = "\(count) viewers"
    }
    
    @objc private func reactionButtonTapped(_ sender: UIButton) {
        let reaction = StreamReaction(rawValue: sender.tag)!
        let streamInfo = streamData.getStreamInfo()
        
        streamManager.sendStreamReaction(
            streamId: streamInfo.streamId,
            reaction: reaction
        ) { result in
            switch result {
            case .success:
                // Animate reaction button
                self.animateReactionButton(sender)
            case .failure(let error):
                print("Failed to send reaction: \(error)")
            }
        }
    }
    
    @IBAction func sendButtonTapped(_ sender: UIButton) {
        guard let message = messageTextField.text, !message.isEmpty else { return }
        
        let streamInfo = streamData.getStreamInfo()
        streamManager.sendChatMessage(
            streamId: streamInfo.streamId,
            message: message
        ) { [weak self] result in
            switch result {
            case .success:
                self?.messageTextField.text = ""
            case .failure(let error):
                self?.showError(error)
            }
        }
    }
    
    private func animateReactionButton(_ button: UIButton) {
        UIView.animate(withDuration: 0.1, animations: {
            button.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
        }) { _ in
            UIView.animate(withDuration: 0.1) {
                button.transform = .identity
            }
        }
    }
    
    private func showError(_ error: Error) {
        let alert = UIAlertController(
            title: "Stream Error",
            message: error.localizedDescription,
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        present(alert, animated: true)
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        streamManager.stopWatching()
    }
}

// Chat table view data source and delegate
extension LiveStreamViewController: UITableViewDataSource, UITableViewDelegate {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return chatMessages.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ChatMessageCell", for: indexPath) as! ChatMessageCell
        cell.configure(with: chatMessages[indexPath.row])
        return cell
    }
}

Stream States & Handling

Understanding different stream states is essential for providing appropriate user experiences:
  • Live State
  • Ended State
  • Scheduled State
{
  "status": "live",
  "streamUrl": "wss://stream.amity.co/...",
  "viewerCount": 245,
  "startedAt": "2024-01-15T10:30:00Z",
  "estimatedDelay": "2-5 seconds"
}

Best Practices for Live Streams

  • Use WebRTC for lowest latency streaming
  • Implement adaptive bitrate based on network conditions
  • Buffer management for smooth playback
  • Handle network interruptions gracefully
  • Rate limiting for chat messages to prevent spam
  • Moderation tools for inappropriate content
  • Emoji reactions for quick engagement
  • Real-time viewer count updates
  • Automatic reconnection for network issues
  • Fallback to recorded version if live fails
  • Clear error messages for users
  • Graceful degradation for unsupported devices
  • Track viewer engagement metrics
  • Monitor stream quality and performance
  • Record chat activity and reactions
  • Generate post-stream analytics reports
For comprehensive live streaming setup and configuration, refer to the Video Streaming Guide.

Poll Post

Poll posts enable interactive user engagement through voting mechanisms integrated directly into the post feed. These posts contain poll data with customizable options and real-time result tracking.

Architecture Overview

Poll Post Structure

Poll posts follow a parent-child architecture where the poll is embedded as a child post:
ComponentDescriptionProperties
Parent PostMain post containerText content, metadata, child references
Poll ChildEmbedded poll dataQuestion, options, vote counts, expiration
Vote ResultsReal-time statisticsVote counts per option, total votes, percentages

Accessing Poll Data

  • iOS
  • Android
  • TypeScript
  • Flutter
import AmitySDK

class PollPostManager {
    private let client: AmityClient
    
    init(client: AmityClient) {
        self.client = client
    }
    
    func getPollFromPost(_ post: AmityPost) -> AmityPoll? {
        // Check if post has poll child
        guard post.childrenPosts?.contains(where: { $0.dataType == .poll }) == true else {
            return nil
        }
        
        // Get poll child post
        if let pollChild = post.childrenPosts?.first(where: { $0.dataType == .poll }),
           let pollData = pollChild.data as? AmityPollData {
            return AmityPoll(data: pollData)
        }
        
        return nil
    }
    
    func displayPollPost(_ post: AmityPost, in viewController: UIViewController) {
        guard let poll = getPollFromPost(post) else {
            // Display regular post without poll
            displayRegularPost(post, in: viewController)
            return
        }
        
        // Create poll UI
        let pollView = createPollView(poll: poll)
        
        // Add vote handling
        pollView.onVoteSelected = { [weak self] optionId in
            self?.submitVote(pollId: poll.pollId, optionId: optionId)
        }
        
        // Display with real-time updates
        setupRealTimeUpdates(for: poll.pollId, in: pollView)
    }
    
    private func createPollView(poll: AmityPoll) -> PollView {
        let pollView = PollView()
        
        // Configure poll UI
        pollView.question = poll.question
        pollView.options = poll.answers.map { answer in
            PollOption(
                id: answer.id,
                text: answer.text,
                voteCount: answer.voteCount,
                isSelected: answer.isVotedByMe
            )
        }
        
        pollView.totalVotes = poll.voteCount
        pollView.isExpired = poll.isClosed
        pollView.timeRemaining = poll.closedIn
        pollView.allowMultipleChoice = poll.answerType == .multiple
        
        return pollView
    }
    
    private func submitVote(pollId: String, optionId: String) {
        let pollRepository = AmityPollRepository(client: client)
        
        pollRepository.vote(pollId: pollId, answerIds: [optionId]) { result in
            DispatchQueue.main.async {
                switch result {
                case .success:
                    // Update UI to reflect vote
                    self.refreshPollData(pollId: pollId)
                case .failure(let error):
                    // Handle vote error
                    self.showError("Failed to submit vote: \(error.localizedDescription)")
                }
            }
        }
    }
    
    private func setupRealTimeUpdates(for pollId: String, in pollView: PollView) {
        let pollRepository = AmityPollRepository(client: client)
        
        // Subscribe to real-time poll updates
        pollRepository.getPoll(withId: pollId)
            .observe { [weak pollView] poll in
                DispatchQueue.main.async {
                    pollView?.updateWithPoll(poll)
                }
            }
    }
}

// Custom poll view component
class PollView: UIView {
    var onVoteSelected: ((String) -> Void)?
    var question: String = "" { didSet { updateUI() } }
    var options: [PollOption] = [] { didSet { updateUI() } }
    var totalVotes: Int = 0 { didSet { updateUI() } }
    var isExpired: Bool = false { didSet { updateUI() } }
    var timeRemaining: TimeInterval = 0 { didSet { updateUI() } }
    var allowMultipleChoice: Bool = false
    
    private func updateUI() {
        // Update poll UI with new data
        // Implementation depends on your UI framework
    }
    
    func updateWithPoll(_ poll: AmityPoll) {
        question = poll.question
        options = poll.answers.map { PollOption(from: $0) }
        totalVotes = poll.voteCount
        isExpired = poll.isClosed
        timeRemaining = poll.closedIn
    }
}

struct PollOption {
    let id: String
    let text: String
    let voteCount: Int
    let isSelected: Bool
    
    init(from answer: AmityPollAnswer) {
        self.id = answer.id
        self.text = answer.text
        self.voteCount = answer.voteCount
        self.isSelected = answer.isVotedByMe
    }
}

Best Practices

  • Visual Feedback: Provide immediate visual feedback when users vote
  • Real-time Updates: Show live vote counts and percentages
  • Accessibility: Include proper ARIA labels and keyboard navigation
  • Mobile Responsive: Ensure poll UI works well on all screen sizes
  • Progress Indicators: Show voting progress with animated bars
  • Efficient Updates: Use selective UI updates for real-time changes
  • Caching Strategy: Cache poll results to reduce API calls
  • Batch Operations: Group multiple vote submissions when possible
  • Memory Management: Properly dispose of real-time subscriptions
  • Network Optimization: Implement retry logic for failed votes
  • Vote Validation: Check poll status before allowing votes
  • Network Errors: Handle offline scenarios gracefully
  • Permission Checks: Verify user can vote before showing options
  • Expired Polls: Disable voting for closed polls
  • Rate Limiting: Implement vote throttling to prevent spam

Error Handling

Error TypeDescriptionRecommended Action
Poll Not FoundPoll child post missing or invalidDisplay post without poll functionality
Poll ExpiredVoting attempted on closed pollShow “Poll Closed” message
Network ErrorVote submission failedRetry with exponential backoff
Permission DeniedUser lacks voting permissionsHide voting interface
Invalid VoteMalformed vote dataValidate input before submission

Real-World Use Cases

Community Feedback

Gather opinions on community decisions, feature requests, or content preferences with engaging poll interfaces.

Content Curation

Let users vote on best content, favorite posts, or trending topics to improve content discovery algorithms.

Event Planning

Use polls to decide on event dates, locations, activities, or gather attendee preferences for better planning.

Product Research

Collect user preferences, feature priorities, or market research data through embedded poll interactions.
For comprehensive poll creation and management features, refer to the Polls Guide. Poll posts automatically inherit all poll features including expiration, vote limits, and moderation controls.