iOS開發 - AVPlayer實現流音頻邊播邊存

来源:http://www.cnblogs.com/xiaopin/archive/2016/09/11/5862370.html
-Advertisement-
Play Games

邊播邊下有三套左右實現思路,本文使用AVPlayer + AVURLAsset實現。 概述 1. AVPlayer簡介 AVPlayer存在於AVFoundation中,可以播放視頻和音頻,可以理解為一個隨身聽 AVPlayer的關聯類: AVAsset:一個抽象類,不能直接使用,代表一個要播放的資 ...


邊播邊下有三套左右實現思路,本文使用AVPlayer + AVURLAsset實現。

概述

1. AVPlayer簡介

  • AVPlayer存在於AVFoundation中,可以播放視頻和音頻,可以理解為一個隨身聽
  • AVPlayer的關聯類:
    • AVAsset:一個抽象類,不能直接使用,代表一個要播放的資源。可以理解為一個磁帶子類AVURLAsset是根據URL生成的包含媒體信息的資源對象。我們就是要通過這個類的代理實現音頻的邊播邊下的
    • AVPlayerItem:可以理解為一個裝在磁帶盒子里的磁帶

2. AVPlayer播放原理

  • 給播放器設置好想要它播放的URL
  • 播放器向URL所在的伺服器發送請求,請求兩個東西
    • 所需音頻片段的起始offset
    • 所需的音頻長度
  • 伺服器根據請求的內容,返回數據
  • 播放器拿到數據拼裝成文件
  • 播放器從拼裝好的文件中,找出現在需要播放的片段,進行播放

3. 邊播邊下的原理

實現邊下邊播,其實就是手動實現AVPlayer的上列播放過程。

  • 當播放器需要預先緩存一些數據的時候,不讓播放器直接向伺服器發起請求,而是向我們自己寫的某個類(暫且稱之為播放器的秘書)發起緩存請求
  • 秘書根據播放器的緩存請求的請求內容,向伺服器發起請求。
  • 伺服器返回秘書所需的數據
  • 秘書把伺服器返回的數據寫進本地的緩存文件
  • 當需要播放某段聲音的時候,向秘書發出播放請求索要這段音頻文件
  • 秘書從本地的緩存文件中找到播放器播放請求所需片段,返回給播放器
  • 播放器拿到數據開心滴播放
  • 當整首歌都緩存完成以後,秘書需要把緩存文件拷貝一份,改個名字,這個文件就是我們所需要的本地持久化文件
  • 下次播放器再播放歌曲的時候,先判斷下本地有木有這個名字的文件,有則播放本地文件,木有則向秘書要數據

技術實現

OK,邊播邊下的原理知道了,我們可以正式寫代碼了~建議先從文末鏈接處把Demo下載下來,對著Demo咱們慢慢道來~

1. 類

共需要三個類:

  • MusicPlayerManagerCEO。單例,負責整個工程所有的播放、暫停、下一曲、結束、判斷應該播放本地文件還是從伺服器拉數據之類的事情
  • RequestLoader:就是上文所說的秘書,負責給播放器提供播放所需的音頻片段,以及找人向伺服器索要數據
  • RequestTask秘書的小弟。負責和伺服器連接、向伺服器請求數據、把請求回來的數據寫到本地緩存文件、把寫完的緩存文件移到持久化目錄去。所有臟活累活都是他做。

2. 方法

先從小弟說起

2.1. RequestTask

2.1.0. 概說

如上文所說,小弟是負責做臟活累活的。 負責和伺服器連接、向伺服器請求數據、把請求回來的數據寫到本地緩存文件、把寫完的緩存文件移到持久化目錄去

2.1.1. 初始化音頻文件持久化文件夾 & 緩存文件

private func _initialTmpFile() {
        do { try NSFileManager.defaultManager().createDirectoryAtPath(StreamAudioConfig.audioDicPath, withIntermediateDirectories: true, attributes: nil) } catch { print("creat dic false -- error:\(error)") }
        if NSFileManager.defaultManager().fileExistsAtPath(StreamAudioConfig.tempPath) {
            try! NSFileManager.defaultManager().removeItemAtPath(StreamAudioConfig.tempPath)
        }
        NSFileManager.defaultManager().createFileAtPath(StreamAudioConfig.tempPath, contents: nil, attributes: nil)
    }

2.1.2. 與伺服器建立連接請求數據

/**
     連接伺服器,請求數據(或拼range請求部分數據)(此方法中會將協議頭修改為http)

     - parameter offset: 請求位置
     */
    public func set(URL url: NSURL, offset: Int) {

        func initialTmpFile() {
            try! NSFileManager.defaultManager().removeItemAtPath(StreamAudioConfig.tempPath)
            NSFileManager.defaultManager().createFileAtPath(StreamAudioConfig.tempPath, contents: nil, attributes: nil)
        }
        _updateFilePath(url)
        self.url = url
        self.offset = offset

        //  如果建立第二次請求,則需初始化緩衝文件
        if taskArr.count >= 1 {
            initialTmpFile()
        }

        //  初始化已下載文件長度
        downLoadingOffset = 0

        //  把stream://xxx的頭換成http://的頭
        let actualURLComponents = NSURLComponents(URL: url, resolvingAgainstBaseURL: false)
        actualURLComponents?.scheme = "http"
        guard let URL = actualURLComponents?.URL else {return}
        let request = NSMutableURLRequest(URL: URL, cachePolicy: NSURLRequestCachePolicy.ReloadIgnoringCacheData, timeoutInterval: 20.0)

        //  若非從頭下載,且視頻長度已知且大於零,則下載offset到videoLength的範圍(拼request參數)
        if offset > 0 && videoLength > 0 {
            request.addValue("bytes=\(offset)-\(videoLength - 1)", forHTTPHeaderField: "Range")
        }

        connection?.cancel()
        connection = NSURLConnection(request: request, delegate: self, startImmediately: false)
        connection?.setDelegateQueue(NSOperationQueue.mainQueue())
        connection?.start()
    }

2.1.3. 響應伺服器的Response頭

      public func connection(connection: NSURLConnection, didReceiveResponse response: NSURLResponse) {
        isFinishLoad = false
        guard response is NSHTTPURLResponse else {return}
        //  解析頭部數據
        let httpResponse = response as! NSHTTPURLResponse
        let dic = httpResponse.allHeaderFields
        let content = dic["Content-Range"] as? String
        let array = content?.componentsSeparatedByString("/")
        let length = array?.last
        //  拿到真實長度
        var videoLength = 0
        if Int(length ?? "0") == 0 {
            videoLength = Int(httpResponse.expectedContentLength)
        } else {
            videoLength = Int(length!)!
        }

        self.videoLength = videoLength
        //TODO: 此處需要修改為真實數據格式 - 從字典中取
        self.mimeType = "video/mp4"
        //  回調
        recieveVideoInfoHandler?(task: self, videoLength: videoLength, mimeType: mimeType!)
        //  連接加入到任務數組中
        taskArr.append(connection)
        //  初始化文件傳輸句柄
        fileHandle = NSFileHandle.init(forWritingAtPath: StreamAudioConfig.tempPath)
    }

2.1.4. 處理伺服器返回的數據 - 寫入緩存文件中

    public func connection(connection: NSURLConnection, didReceiveData data: NSData) {

        //  定址到文件末尾
        self.fileHandle?.seekToEndOfFile()
        self.fileHandle?.writeData(data)
        self.downLoadingOffset += data.length
        self.receiveVideoDataHandler?(task: self)

//        print("線程 - \(NSThread.currentThread())")

        //  註意,這裡用子線程有問題
        let queue = dispatch_queue_create("com.azen.taskConnect", DISPATCH_QUEUE_SERIAL)
        dispatch_async(queue) {
//            //  定址到文件末尾
//            self.fileHandle?.seekToEndOfFile()
//            self.fileHandle?.writeData(data)
//            self.downLoadingOffset += data.length
//            self.receiveVideoDataHandler?(task: self)
//            let thread = NSThread.currentThread()
//            print("線程 - \(thread)")
        }

2.1.5. 伺服器文件返回完畢,把緩存文件放入持久化文件夾

    public func connectionDidFinishLoading(connection: NSURLConnection) {
        func tmpPersistence() {
            isFinishLoad = true
            let fileName = url?.lastPathComponent
//            let movePath = audioDicPath.stringByAppendingPathComponent(fileName ?? "undefine.mp4")
            let movePath = StreamAudioConfig.audioDicPath + "/\(fileName ?? "undefine.mp4")"
            _ = try? NSFileManager.defaultManager().removeItemAtPath(movePath)

            var isSuccessful = true
            do { try NSFileManager.defaultManager().copyItemAtPath(StreamAudioConfig.tempPath, toPath: movePath) } catch {
                isSuccessful = false
                print("tmp文件持久化失敗")
            }
            if isSuccessful {
                print("持久化文件成功!路徑 - \(movePath)")
            }
        }

        if taskArr.count < 2 {
            tmpPersistence()
        }

        receiveVideoFinishHanlder?(task: self)
    }

其他

其他方法包括斷線重連以及公開一個cancel方法cancel掉和伺服器的連接

2.2. RequestTask

2.2.0. 概說

秘書要乾的最主要的事情就是響應播放器老大的號令,所有方法都是圍繞著播放器老大來的。秘書需要遵循AVAssetResourceLoaderDelegate協議才能被錄用。

2.2.1. 代理方法,播放器需要緩存數據的時候,會調這個方法

這個方法其實是播放器在說:小秘呀,我想要這段音頻文件。你能現在給我還是等等給我啊?
一定要返回:true,告訴播放器,我等等給你。
然後,立馬找本地緩存文件里有木有這段數據,有把數據拿給播放器,如果木有,則派秘書的小弟向伺服器要。
具體實現代碼有點多,這裡就不全部貼出來了。可以去看看文末的Demo記得賞顆星喲~

    /**
     播放器問:是否應該等這requestResource載入完再說?
     這裡會出現很多個loadingRequest請求, 需要為每一次請求作出處理

     - parameter resourceLoader: 資源管理器
     - parameter loadingRequest: 每一小塊數據的請求

     - returns: <#return value description#>
     */
    public func resourceLoader(resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
        //  添加請求到隊列
        pendingRequset.append(loadingRequest)
        //  處理請求
        _dealWithLoadingRequest(loadingRequest)
        print("----\(loadingRequest)")
        return true
    }

2.2.2. 代理方法,播放器關閉了下載請求

    /**
     播放器關閉了下載請求
     播放器關閉一個舊請求,都會發起一到多個新請求,除非已經播放完畢了

     - parameter resourceLoader: 資源管理器
     - parameter loadingRequest: 待關請求
     */
    public func resourceLoader(resourceLoader: AVAssetResourceLoader, didCancelLoadingRequest loadingRequest: AVAssetResourceLoadingRequest) {
        guard let index = pendingRequset.indexOf(loadingRequest) else {return}
        pendingRequset.removeAtIndex(index)
    }

2.3. MusicPlayerManager

2.3.0. 概說

負責調度所有播放器的,負責App中的一切涉及音頻播放的事件
唔。。犯個小懶。。代碼直接貼上來咯~要趕不上樓下的538路公交啦~~謝謝大家體諒哦~

public class MusicPlayerManager: NSObject {


    //  public var status

    public var currentURL: NSURL? {
        get {
            guard let currentIndex = currentIndex, musicURLList = musicURLList where currentIndex < musicURLList.count else {return nil}
            return musicURLList[currentIndex]
        }
    }

    /**播放狀態,用於需要獲取播放器狀態的地方KVO*/
    public var status: ManagerStatus = .Non
    /**播放進度*/
    public var progress: CGFloat {
        get {
            if playDuration > 0 {
                let progress = playTime / playDuration
                return progress
            } else {
                return 0
            }
        }
    }
    /**已播放時長*/
    public var playTime: CGFloat = 0
    /**總時長*/
    public var playDuration: CGFloat = CGFloat.max
    /**緩衝時長*/
    public var tmpTime: CGFloat = 0

    public var playEndConsul: (()->())?
    /**強引用控制器,防止被銷毀*/
    public var currentController: UIViewController?

    //  private status
    private var currentIndex: Int?
    private var currentItem: AVPlayerItem? {
        get {
            if let currentURL = currentURL {
                let item = getPlayerItem(withURL: currentURL)
                return item
            } else {
                return nil
            }
        }
    }

    private var musicURLList: [NSURL]?

    //  basic element
    public var player: AVPlayer?

    private var playerStatusObserver: NSObject?
    private var resourceLoader: RequestLoader = RequestLoader()
    private var currentAsset: AVURLAsset?
    private var progressCallBack: ((tmpProgress: Float?, playProgress: Float?)->())?

    public class var sharedInstance: MusicPlayerManager {
        struct Singleton {
            static let instance = MusicPlayerManager()
        }
        //  後臺播放
        let session = AVAudioSession.sharedInstance()
        do { try session.setActive(true) } catch { print(error) }
        do { try session.setCategory(AVAudioSessionCategoryPlayback) } catch { print(error) }
        return Singleton.instance
    }

    public enum ManagerStatus {
        case Non, LoadSongInfo, ReadyToPlay, Play, Pause, Stop
    }
}

// MARK: - basic public funcs
extension MusicPlayerManager {
    /**
     開始播放
     */
    public func play(musicURL: NSURL?) {
        guard let musicURL = musicURL else {return}
        if let index = getIndexOfMusic(music: musicURL) {   //   歌曲在隊列中,則按順序播放
            currentIndex = index
        } else {
            putMusicToArray(music: musicURL)
            currentIndex = 0
        }
        playMusicWithCurrentIndex()
    }

    public func play(musicURL: NSURL?, callBack: ((tmpProgress: Float?, playProgress: Float?)->())?) {
        play(musicURL)
        progressCallBack = callBack
    }

    public func next() {
        currentIndex = getNextIndex()
        playMusicWithCurrentIndex()
    }

    public func previous() {
        currentIndex = getPreviousIndex()
        playMusicWithCurrentIndex()
    }
    /**
     繼續
     */
    public func goOn() {
        player?.rate = 1
    }
    /**
     暫停 - 可繼續
     */
    public func pause() {
        player?.rate = 0
    }
    /**
     停止 - 無法繼續
     */
    public func stop() {
        endPlay()
    }
}

// MARK: - private funcs
extension MusicPlayerManager {

    private func putMusicToArray(music URL: NSURL) {
        if musicURLList == nil {
            musicURLList = [URL]
        } else {
            musicURLList!.insert(URL, atIndex: 0)
        }
    }

    private func getIndexOfMusic(music URL: NSURL) -> Int? {
        let index = musicURLList?.indexOf(URL)
        return index
    }

    private func getNextIndex() -> Int? {
        if let musicURLList = musicURLList where musicURLList.count > 0 {
            if let currentIndex = currentIndex where currentIndex + 1 < musicURLList.count {
                return currentIndex + 1
            } else {
                return 0
            }
        } else {
            return nil
        }
    }

    private func getPreviousIndex() -> Int? {
        if let currentIndex = currentIndex {
            if currentIndex - 1 >= 0 {
                return currentIndex - 1
            } else {
                return musicURLList?.count ?? 1 - 1
            }
        } else {
            return nil
        }
    }

    /**
     從頭播放音樂列表
     */
    private func replayMusicList() {
        guard let musicURLList = musicURLList where musicURLList.count > 0 else {return}
        currentIndex = 0
        playMusicWithCurrentIndex()
    }
    /**
     播放當前音樂
     */
    private func playMusicWithCurrentIndex() {
        guard let currentURL = currentURL else {return}
        //  結束上一首
        endPlay()
        player = AVPlayer(playerItem: getPlayerItem(withURL: currentURL))
        observePlayingItem()
    }
    /**
     本地不存在,返回nil,否則返回本地URL
     */
    private func getLocationFilePath(url: NSURL) -> NSURL? {
        let fileName = url.lastPathComponent
        let path = StreamAudioConfig.audioDicPath + "/\(fileName ?? "tmp.mp4")"
        if NSFileManager.defaultManager().fileExistsAtPath(path) {
            let url = NSURL.init(fileURLWithPath: path)
            return url
        } else {
            return nil
        }
    }

    private func getPlayerItem(withURL musicURL: NSURL) -> AVPlayerItem {

        if let locationFile = getLocationFilePath(musicURL) {
            let item = AVPlayerItem(URL: locationFile)
            return item
        } else {
            let playURL = resourceLoader.getURL(url: musicURL)!  //  轉換協議頭
            let asset = AVURLAsset(URL: playURL)
            currentAsset = asset
            asset.resourceLoader.setDelegate(resourceLoader, queue: dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0))
            let item = AVPlayerItem(asset: asset)
            return item
        }
    }

    private func setupPlayer(withURL musicURL: NSURL) {
        let songItem = getPlayerItem(withURL: musicURL)
        player = AVPlayer(playerItem: songItem)
    }

    private func playerPlay() {
        player?.play()
    }

    private func endPlay() {
        status = ManagerStatus.Stop
        player?.rate = 0
        removeObserForPlayingItem()
        player?.replaceCurrentItemWithPlayerItem(nil)
        resourceLoader.cancel()
        currentAsset?.resourceLoader.setDelegate(nil, queue: nil)

        progressCallBack = nil
        resourceLoader = RequestLoader()
        playDuration = 0
        playTime = 0
        playEndConsul?()
        player = nil
    }
}

extension MusicPlayerManager {
    public override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
        guard object is AVPlayerItem else {return}
        let item = object as! AVPlayerItem
        if keyPath == "status" {
            if item.status == AVPlayerItemStatus.ReadyToPlay {
                status = .ReadyToPlay
                print("ReadyToPlay")
                let duration = item.duration
                playerPlay()
                print(duration)
            } else if item.status == AVPlayerItemStatus.Failed {
                status = .Stop
                print("Failed")
                stop()
            }
        } else if keyPath == "loadedTimeRanges" {
            let array = item.loadedTimeRanges
            guard let timeRange = array.first?.CMTimeRangeValue else {return}  //  緩衝時間範圍
            let totalBuffer = CMTimeGetSeconds(timeRange.start) + CMTimeGetSeconds(timeRange.duration)    //  當前緩衝長度
            tmpTime = CGFloat(tmpTime)
            print("共緩衝 - \(totalBuffer)")
            let tmpProgress = tmpTime / playDuration
            progressCallBack?(tmpProgress: Float(tmpProgress), playProgress: nil)
        }
    }

    private func observePlayingItem() {
        guard let currentItem = self.player?.currentItem else {return}
        //  KVO監聽正在播放的對象狀態變化
        currentItem.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.New, context: nil)
        //  監聽player播放情況
        playerStatusObserver = player?.addPeriodicTimeObserverForInterval(CMTimeMake(1, 1), queue: dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), usingBlock: { [weak self] (time) in
            guard let `self` = self else {return}
            //  獲取當前播放時間
            self.status = .Play
            let currentTime = CMTimeGetSeconds(time)
            let totalTime = CMTimeGetSeconds(currentItem.duration)
            self.playDuration = CGFloat(totalTime)
            self.playTime = CGFloat(currentTime)
            print("current time ---- \(currentTime) ---- tutalTime ---- \(totalTime)")
            self.progressCallBack?(tmpProgress: nil, playProgress: Float(self.progress))
            if totalTime - currentTime < 0.1 {
                self.endPlay()
            }
            }) as? NSObject
        //  監聽緩存情況
        currentItem.addObserver(self, forKeyPath: "loadedTimeRanges", options: NSKeyValueObservingOptions.New, context: nil)
    }

    private func removeObserForPlayingItem() {
        guard let currentItem = self.player?.currentItem else {return}
        currentItem.removeObserver(self, forKeyPath: "status")
        if playerStatusObserver != nil {
            player?.removeTimeObserver(playerStatusObserver!)
            playerStatusObserver = nil
        }
        currentItem.removeObserver(self, forKeyPath: "loadedTimeRanges")
    }
}

public struct StreamAudioConfig {
    static let audioDicPath: String = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true).last! + "/streamAudio"  //  緩衝文件夾
    static let tempPath: String = audioDicPath + "/temp.mp4"    //  緩衝文件路徑 - 非持久化文件路徑 - 當前邏輯下,有且只有一個緩衝文件

}

iOS音頻邊播邊下Demo,戳這裡~

 

本人學習收藏

文/Azen(簡書作者)
原文鏈接:http://www.jianshu.com/p/4f586d63a532
著作權歸作者所有,轉載請聯繫作者獲得授權,並標註“簡書作者”。
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 大家看到微信首頁切換效果有沒有覺得很炫,滑動切換,點擊底部bar瞬間切換,滑動切換漸變效果,線上效果圖: 之前也在博客上看到別人的實現,再次基礎上,我做了些優化。首先說下實現原理,大神略過,o(╯□╰)o 頁面上看到的三個頁面是三個Fragment, 左右滑動使用viewpager,相信大家也都是這 ...
  • 一、使用純代碼方式 initWithFrame:中添加子控制項 layoutSubViews中設置子控制項的fame 對外設置數據介面,重寫setter方法給子控制項設置數據顯示 在的viewController裡面使用init/initWithFrame:方法創建自定義類,並且給自定義類的frame賦值 ...
  • 二手交易平臺 我的畢業設計項目安卓源碼,二手交易平臺,dagger2+mvp+Bmob後臺雲搭建,集成了百度地圖,友盟三方登錄等 系統架構 Dagger2+MVP分層,完成了一次正常的retrofit下的天氣信息的請求,其餘請求後臺均基於Bmob雲後臺,圖片在水平方向可滾動 說明 使用請尊重本人技術 ...
  • 視圖基礎 視圖層次結構 任何應用有且只有一個 UIWindow 對象。 UIWindow 對象就像是一個容器,負責包含應用中的所有的視圖。應用需要在啟動時創建並設置 UIWindow 對象,然後為其添加其他視圖。 加入視窗的視圖會成為該視窗的子視圖。視窗的子視圖還可以有自己的子視圖,從而構成一個以  ...
  • 1、下拉列表框(Spinner) 項目佈局 添加相應代碼: 2、輸入內容自動完成文本框(AutoCompleteTextView) AutoCompleteTextView和EditText組件類似,都可以輸入文本。但AutoCompleteTextView組件可以和一個字元串數組或List對象綁定 ...
  • 雖然騷尼手機賣的不怎麼樣,但是有些東西還是做的挺好的,工業設計就不用說了,索尼的相冊的雙指任意縮放功能也是尤其炫酷。其桌面小部件滾動相冊我覺得也挺好的,比谷歌原生的相冊牆功能好多了,網上搜了一下也沒發現有人寫這個,於是,下麵就介紹下我的高A貨。 首先是效果圖: 主要手勢操作有: 該小部件的主要優點: ...
  • delegate的屬性不是weakNSTimer沒有invalidateblock中的強引用調用了performSelector,退出時沒有cancelPerformSelectorsWithTargetThis method sets up a timer to perform the aSele... ...
  • 1.安裝genymotion-vbox,選擇安裝目錄。 具體安裝過程可見http://www.cnblogs.com/wuyudong/p/5601897.html 2.登錄並創建模擬器 3.將相關插件(插件下載地址:http://pan.baidu.com/s/1jGvYaDg)拷貝到adt下麵的 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...