Winui3 FFmpeg.autogen 解析音頻,使用NAudio播放;

来源:https://www.cnblogs.com/chifan/archive/2022/07/25/16516018.html
-Advertisement-
Play Games

在上兩篇文章中已經將播放視頻的功能實現了,今天我就來講解一下如何通過FFmpeg來解析音頻內容,並且用NAudio來進行音頻播放; 效果圖 雖然效果圖是gif並不能 聽到音頻播放的內容,不過可以從圖中看到已經是實現了音頻的播放,暫停,停止已經更改進度的內容了; 一。添加NAudio庫: 一.音頻解碼 ...


     在上兩篇文章中已經將播放視頻的功能實現了,今天我就來講解一下如何通過FFmpeg來解析音頻內容,並且用NAudio來進行音頻播放;

     效果圖

     雖然效果圖是gif並不能

 

聽到音頻播放的內容,不過可以從圖中看到已經是實現了音頻的播放,暫停,停止已經更改進度的內容了;

一。添加NAudio庫:

  

一.音頻解碼播放流程

   可以從流程圖中看到音頻的解碼跟視頻的解碼是差不多的,只有是重採樣跟將幀數據轉換成位元組數組這兩個步驟有區別而已。

  1.初始化音頻解碼

public void InitDecodecAudio(string path)
        {
            int error = 0;
            //創建一個 媒體格式上下文
            format = ffmpeg.avformat_alloc_context();
            var tempFormat = format;
            //打開媒體文件
            error = ffmpeg.avformat_open_input(&tempFormat, path, null, null);
            if (error < 0)
            {
                Debug.WriteLine("打開媒體文件失敗");
                return;
            }
            //嗅探媒體信息
            ffmpeg.avformat_find_stream_info(format, null);
            AVCodec* codec;
            //獲取音頻流索引
            audioStreamIndex = ffmpeg.av_find_best_stream(format, AVMediaType.AVMEDIA_TYPE_AUDIO, -1, -1, &codec, 0);
            if (audioStreamIndex < 0)
            {
                Debug.WriteLine("沒有找到音頻流");
                return;
            }
            //獲取音頻流
            audioStream = format->streams[audioStreamIndex];
            //創建解碼上下文
            codecContext = ffmpeg.avcodec_alloc_context3(codec);
            //將音頻流裡面的解碼器參數設置到 解碼器上下文中
            error = ffmpeg.avcodec_parameters_to_context(codecContext, audioStream->codecpar);
            if (error < 0)
            {
                Debug.WriteLine("設置解碼器參數失敗");
            }
            error = ffmpeg.avcodec_open2(codecContext, codec, null);
            //媒體時長
            Duration = TimeSpan.FromMilliseconds(format->duration / 1000);
            //編解碼id
            CodecId = codec->id.ToString();
            //解碼器名字
            CodecName = ffmpeg.avcodec_get_name(codec->id);
            //比特率
            Bitrate = codecContext->bit_rate;
            //音頻通道數
            Channels = codecContext->channels;
            //通道佈局類型
            ChannelLyaout = codecContext->channel_layout;
            //音頻採樣率
            SampleRate = codecContext->sample_rate;
            //音頻採樣格式
            SampleFormat = codecContext->sample_fmt;
            //採樣次數  //獲取給定音頻參數所需的緩衝區大小。
            BitsPerSample = ffmpeg.av_samples_get_buffer_size(null, 2, codecContext->frame_size, AVSampleFormat.AV_SAMPLE_FMT_S16, 1);
            //創建一個指針
            audioBuffer = Marshal.AllocHGlobal((int)BitsPerSample);
            bufferPtr = (byte*)audioBuffer;
            //初始化音頻重採樣轉換器
            InitConvert((int)ChannelLyaout, AVSampleFormat.AV_SAMPLE_FMT_S16, (int)SampleRate, (int)ChannelLyaout, SampleFormat, (int)SampleRate);
            //創建一個包和幀指針
            packet = ffmpeg.av_packet_alloc();
            frame = ffmpeg.av_frame_alloc();
            State = MediaState.Read;
        }
InitDecodecAudio

   在初始化各個結構的代碼其實是跟解碼視頻的流程差不多的,只是獲取媒體流的類型從視頻類型更改成了音頻類型,和媒體的信息從視頻改為了音頻信息,並且獲取了音頻的採樣次數和創建了一個用於後續讀取音頻數據的指針。

  2.初始化音頻重採樣轉換器。

 /// <summary>
        /// 初始化重採樣轉換器
        /// </summary>
        /// <param name="occ">輸出的通道類型</param>
        /// <param name="osf">輸出的採樣格式</param>
        /// <param name="osr">輸出的採樣率</param>
        /// <param name="icc">輸入的通道類型</param>
        /// <param name="isf">輸入的採樣格式</param>
        /// <param name="isr">輸入的採樣率</param>
        /// <returns></returns>
        bool InitConvert(int occ, AVSampleFormat osf, int osr, int icc, AVSampleFormat isf, int isr)
        {
            //創建一個重採樣轉換器
            convert = ffmpeg.swr_alloc();
            //設置重採樣轉換器參數
            convert = ffmpeg.swr_alloc_set_opts(convert, occ, osf, osr, icc, isf, isr, 0, null);
            if (convert == null)
                return false;
            //初始化重採樣轉換器
            ffmpeg.swr_init(convert);
            return true;
        }
InitConvert

     根據輸入參數初始化了一個 SwrContext結構的音頻轉換器,跟 視頻的SwsContext結構是不同的。

  3。從音頻幀讀取數據。

 public byte[] FrameConvertBytes(AVFrame* sourceFrame)
        {
            var tempBufferPtr = bufferPtr;
            //重採樣音頻
            var outputSamplesPerChannel = ffmpeg.swr_convert(convert, &tempBufferPtr, frame->nb_samples, sourceFrame->extended_data, sourceFrame->nb_samples);
            //獲取重採樣後的音頻數據大小
            var outPutBufferLength = ffmpeg.av_samples_get_buffer_size(null, 2, outputSamplesPerChannel, AVSampleFormat.AV_SAMPLE_FMT_S16, 1);
            if (outputSamplesPerChannel < 0)
                return null;
            byte[] bytes = new byte[outPutBufferLength];
            //從記憶體中讀取轉換後的音頻數據
            Marshal.Copy(audioBuffer, bytes, 0, bytes.Length);
            return bytes;
        }
FrameConvertBytes

      調用ffmpeg.swr_convert()將音頻幀通過重採樣後音頻的數據大小會發生改變的,需要再次調用 ffmpeg.av_samples_get_buffer_size() 來重新計算 音頻數據大小,並通過指針位置獲取數據;

     4.聲明音頻播放組件

 //NAudio音頻播放組件
        private WaveOut waveOut;
        private BufferedWaveProvider bufferedWaveProvider;

  5.線上程任務上迴圈讀取音頻幀解碼成位元組數組並 向bufferedAveProvider 添加音頻樣本,當添加的音頻數據大於預設的數據量則將緩存內的數據都清除掉。

    PlayTask = new Task(() =>
            {
                while (true)
                {
                    //播放中
                    if (audio.IsPlaying)
                    {
                        //獲取下一幀視頻
                        if (audio.TryReadNextFrame(out var frame))
                        {
                            var bytes = audio.FrameConvertBytes(&frame);
                            if (bytes == null)
                                continue;
                            if (bufferedWaveProvider.BufferLength <= bufferedWaveProvider.BufferedBytes+bytes.Length)
                            {
                                bufferedWaveProvider.ClearBuffer();
                            }
                            bufferedWaveProvider.AddSamples(bytes, 0, bytes.Length);//向緩存中添加音頻樣本
                        }
                    }
                }
            });
            PlayTask.Start();
PlayTask

二.音頻讀取解碼整個流程 DecodecAudio 類

public unsafe class DecodecAudio : IMedia
    {
        //媒體格式容器
        AVFormatContext* format;
        //解碼上下文
        AVCodecContext* codecContext;
        AVStream* audioStream;
        //媒體數據包
        AVPacket* packet;
        AVFrame* frame;
        SwrContext* convert;
        int audioStreamIndex;
        bool isNextFrame = true;
        //播放上一幀的時間
        TimeSpan lastTime;
        TimeSpan OffsetClock;
        object SyncLock = new object();
        Stopwatch clock = new Stopwatch();
        bool isNexFrame = true;
        public event IMedia.MediaHandler MediaCompleted;
        //是否是正在播放中
        public bool IsPlaying { get; protected set; }
        /// <summary>
        /// 媒體狀態
        /// </summary>
        public MediaState State { get; protected set; }
        /// <summary>
        /// 幀播放時長
        /// </summary>
        public TimeSpan frameDuration { get; protected set; }
        /// <summary>
        /// 媒體時長
        /// </summary>
        public TimeSpan Duration { get; protected set; }
        /// <summary>
        /// 播放位置
        /// </summary>
        public TimeSpan Position { get => OffsetClock + clock.Elapsed; }
        /// <summary>
        /// 解碼器名字
        /// </summary>
        public string CodecName { get; protected set; }
        /// <summary>
        /// 解碼器Id
        /// </summary>
        public string CodecId { get; protected set; }
        /// <summary>
        /// 比特率
        /// </summary>
        public long Bitrate { get; protected set; }
        //通道數
        public int Channels { get; protected set; }
        //採樣率
        public long SampleRate { get; protected set; }
        //採樣次數
        public long BitsPerSample { get; protected set; }
        //通道佈局
        public ulong ChannelLyaout { get; protected set; }
        /// <summary>
        /// 採樣格式
        /// </summary>
        public AVSampleFormat SampleFormat { get; protected set; }
        public void InitDecodecAudio(string path)
        {
            int error = 0;
            //創建一個 媒體格式上下文
            format = ffmpeg.avformat_alloc_context();
            var tempFormat = format;
            //打開媒體文件
            error = ffmpeg.avformat_open_input(&tempFormat, path, null, null);
            if (error < 0)
            {
                Debug.WriteLine("打開媒體文件失敗");
                return;
            }
            //嗅探媒體信息
            ffmpeg.avformat_find_stream_info(format, null);
            AVCodec* codec;
            //獲取音頻流索引
            audioStreamIndex = ffmpeg.av_find_best_stream(format, AVMediaType.AVMEDIA_TYPE_AUDIO, -1, -1, &codec, 0);
            if (audioStreamIndex < 0)
            {
                Debug.WriteLine("沒有找到音頻流");
                return;
            }
            //獲取音頻流
            audioStream = format->streams[audioStreamIndex];
            //創建解碼上下文
            codecContext = ffmpeg.avcodec_alloc_context3(codec);
            //將音頻流裡面的解碼器參數設置到 解碼器上下文中
            error = ffmpeg.avcodec_parameters_to_context(codecContext, audioStream->codecpar);
            if (error < 0)
            {
                Debug.WriteLine("設置解碼器參數失敗");
            }
            error = ffmpeg.avcodec_open2(codecContext, codec, null);
            //媒體時長
            Duration = TimeSpan.FromMilliseconds(format->duration / 1000);
            //編解碼id
            CodecId = codec->id.ToString();
            //解碼器名字
            CodecName = ffmpeg.avcodec_get_name(codec->id);
            //比特率
            Bitrate = codecContext->bit_rate;
            //音頻通道數
            Channels = codecContext->channels;
            //通道佈局類型
            ChannelLyaout = codecContext->channel_layout;
            //音頻採樣率
            SampleRate = codecContext->sample_rate;
            //音頻採樣格式
            SampleFormat = codecContext->sample_fmt;
            //採樣次數  //獲取給定音頻參數所需的緩衝區大小。
            BitsPerSample = ffmpeg.av_samples_get_buffer_size(null, 2, codecContext->frame_size, AVSampleFormat.AV_SAMPLE_FMT_S16, 1);
            //創建一個指針
            audioBuffer = Marshal.AllocHGlobal((int)BitsPerSample);
            bufferPtr = (byte*)audioBuffer;
            //初始化音頻重採樣轉換器
            InitConvert((int)ChannelLyaout, AVSampleFormat.AV_SAMPLE_FMT_S16, (int)SampleRate, (int)ChannelLyaout, SampleFormat, (int)SampleRate);
            //創建一個包和幀指針
            packet = ffmpeg.av_packet_alloc();
            frame = ffmpeg.av_frame_alloc();
            State = MediaState.Read;
        }
        //緩衝區指針
        IntPtr audioBuffer;
        //緩衝區句柄
        byte* bufferPtr;
        /// <summary>
        /// 初始化重採樣轉換器
        /// </summary>
        /// <param name="occ">輸出的通道類型</param>
        /// <param name="osf">輸出的採樣格式</param>
        /// <param name="osr">輸出的採樣率</param>
        /// <param name="icc">輸入的通道類型</param>
        /// <param name="isf">輸入的採樣格式</param>
        /// <param name="isr">輸入的採樣率</param>
        /// <returns></returns>
        bool InitConvert(int occ, AVSampleFormat osf, int osr, int icc, AVSampleFormat isf, int isr)
        {
            //創建一個重採樣轉換器
            convert = ffmpeg.swr_alloc();
            //設置重採樣轉換器參數
            convert = ffmpeg.swr_alloc_set_opts(convert, occ, osf, osr, icc, isf, isr, 0, null);
            if (convert == null)
                return false;
            //初始化重採樣轉換器
            ffmpeg.swr_init(convert);
            return true;
        }
        /// <summary>
        /// 嘗試讀取下一幀
        /// </summary>
        /// <param name="outFrame"></param>
        /// <returns></returns>
        public bool TryReadNextFrame(out AVFrame outFrame)
        {
            if (lastTime == TimeSpan.Zero)
            {
                lastTime = Position;
                isNextFrame = true;
            }
            else
            {
                if (Position - lastTime >= frameDuration)
                {
                    lastTime = Position;
                    isNextFrame = true;
                }
                else
                {
                    outFrame = *frame;
                    return false;
                }
            }
            if (isNextFrame)
            {

                lock (SyncLock)
                {
                    int result = -1;
                    //清理上一幀的數據
                    ffmpeg.av_frame_unref(frame);
                    while (true)
                    {
                        //清理上一幀的數據包
                        ffmpeg.av_packet_unref(packet);
                        //讀取下一幀,返回一個int 查看讀取數據包的狀態
                        result = ffmpeg.av_read_frame(format, packet);
                        //讀取了最後一幀了,沒有數據了,退出讀取幀
                        if (result == ffmpeg.AVERROR_EOF || result < 0)
                        {
                            outFrame = *frame;
                            StopPlay();
                            return false;
                        }
                        //判斷讀取的幀數據是否是視頻數據,不是則繼續讀取
                        if (packet->stream_index != audioStreamIndex)
                            continue;
                        //將包數據發送給解碼器解碼
                        ffmpeg.avcodec_send_packet(codecContext, packet);
                        //從解碼器中接收解碼後的幀
                        result = ffmpeg.avcodec_receive_frame(codecContext, frame);
                        if (result < 0)
                            continue;
                        //計算當前幀播放的時長
                        frameDuration = TimeSpan.FromTicks((long)Math.Round(TimeSpan.TicksPerMillisecond * 1000d * frame->nb_samples / frame->sample_rate, 0));
                        outFrame = *frame;
                        return true;
                    }
                }
            }
            else
            {
                outFrame = *frame;
                return false;
            }
        }
        void StopPlay()
        {
            lock (SyncLock)
            {
                if (State == MediaState.None) return;
                IsPlaying = false;
                OffsetClock = TimeSpan.FromSeconds(0);
                clock.Reset();
                clock.Stop();
                var tempFormat = format;
                ffmpeg.avformat_free_context(tempFormat);
                format = null;
                var tempCodecContext = codecContext;
                ffmpeg.avcodec_free_context(&tempCodecContext);
                var tempPacket = packet;
                ffmpeg.av_packet_free(&tempPacket);
                var tempFrame = frame;
                ffmpeg.av_frame_free(&tempFrame);
                var tempConvert = convert;
                ffmpeg.swr_free(&tempConvert);

                Marshal.FreeHGlobal(audioBuffer);
                bufferPtr = null;

                audioStream = null;
                audioStreamIndex = -1;
                //視頻時長
                Duration = TimeSpan.FromMilliseconds(0);
                //編解碼器名字
                CodecName = String.Empty;
                CodecId = String.Empty;
                //比特率
                Bitrate = 0;
                //幀率
              
                Channels = 0;
                ChannelLyaout = 0;
                SampleRate = 0;
                BitsPerSample = 0;
                State = MediaState.None;

                lastTime = TimeSpan.Zero;
                MediaCompleted?.Invoke(Duration);
            }
        }
        /// <summary>
        /// 更改進度
        /// </summary>
        /// <param name="seekTime">更改到的位置(秒)</param>
        public void SeekProgress(int seekTime)
        {
            if (format == null || audioStreamIndex == null)
                return;
            lock (SyncLock)
            {
                IsPlaying = false;//將視頻暫停播放
                clock.Stop();
                //將秒數轉換成視頻的時間戳
                var timestamp = seekTime / ffmpeg.av_q2d(audioStream->time_base);
                //將媒體容器裡面的指定流(視頻)的時間戳設置到指定的位置,並指定跳轉的方法;
                ffmpeg.av_seek_frame(format, audioStreamIndex, (long)timestamp, ffmpeg.AVSEEK_FLAG_BACKWARD | ffmpeg.AVSEEK_FLAG_FRAME);
                ffmpeg.av_frame_unref(frame);//清除上一幀的數據
                ffmpeg.av_packet_unref(packet); //清除上一幀的數據包
                int error = 0;
                //迴圈獲取幀數據,判斷獲取的幀時間戳已經大於給定的時間戳則說明已經到達了指定的位置則退出迴圈
                while (packet->pts < timestamp)
                {
                    do
                    {
                        do
                        {
                            ffmpeg.av_packet_unref(packet);//清除上一幀數據包
                            error = ffmpeg.av_read_frame(format, packet);//讀取數據
                            if (error == ffmpeg.AVERROR_EOF)//是否是到達了視頻的結束位置
                                return;
                        } while (packet->stream_index != audioStreamIndex);//判斷當前獲取的數據是否是視頻數據
                        ffmpeg.avcodec_send_packet(codecContext, packet);//將數據包發送給解碼器解碼
                        error = ffmpeg.avcodec_receive_frame(codecContext, frame);//從解碼器獲取解碼後的幀數據
                    } while (error == ffmpeg.AVERROR(ffmpeg.EAGAIN));
                }
                OffsetClock = TimeSpan.FromSeconds(seekTime);//設置時間偏移
                clock.Restart();//時鐘從新開始
                IsPlaying = true;//視頻開始播放
                lastTime = TimeSpan.Zero;
            }
        }
        /// <summary>
        /// 將音頻幀轉換成位元組數組
        /// </summary>
        /// <param name="sourceFrame"></param>
        /// <returns></returns>
        public byte[] FrameConvertBytes(AVFrame* sourceFrame)
        {
            var tempBufferPtr = bufferPtr;
            //重採樣音頻
            var outputSamplesPerChannel = ffmpeg.swr_convert(convert, &tempBufferPtr, frame->nb_samples, sourceFrame->extended_data, sourceFrame->nb_samples);
            //獲取重採樣後的音頻數據大小
            var outPutBufferLength = ffmpeg.av_samples_get_buffer_size(null, 2, outputSamplesPerChannel, AVSampleFormat.AV_SAMPLE_FMT_S16, 1);
            if (outputSamplesPerChannel < 0)
                return null;
            byte[] bytes = new byte[outPutBufferLength];
            //從記憶體中讀取轉換後的音頻數據
            Marshal.Copy(audioBuffer, bytes, 0, bytes.Length);
            return bytes;
        }
        public void Play()
        {
            if (State == MediaState.Play)
                return;
            clock.Start();
            IsPlaying = true;
            State = MediaState.Play;

        }
        public void Pause()
        {
            if (State != MediaState.Play)
                return;
            IsPlaying = false;
            OffsetClock = clock.Elapsed;
            clock.

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、Http 1.什麼事Http Http(超文本傳輸協議)是一個簡單的請求-響應協議,它通常運行在TCP之上 文本:html,字元串,~.. 超文本:圖片,音樂,視屏,定位,地圖... 埠為80 Https:安全的,埠號443 2.兩個時代 http1.0 HTTP/1.0:客戶端可以與web ...
  • Java流程式控制制 1.用戶交互Scanner java.util.Scanner是Java5的新特征,我們可以通過Scannner類來獲取用戶的輸入。 基本語法: Scanner s = new Scanner(System.in); 通過Scanner類的next()與nextLine()方法獲取 ...
  • 一: 背景 最近在看 C++ 的右值引用和移動構造函數,感覺這東西一時半會還挺難理解的,可能是沒踩過這方面的坑,所以沒有那麼大的深有體會,不管怎麼說,這一篇我試著聊一下。 二: 右值引用 1. 它到底解決了什麼問題? 在其他編程語言中,很少聽到 右值引用 這個詞,我個人感覺還是 C++ 這個 值類型 ...
  • 引言 今天來談談設計模式中的單例模式,溫故知新,以免生疏。 軟體設計領域的四位世界級大師Gang Of Four (GoF):Erich Gamma,Richard Helm,Ralph Johnson,John Vlissides四人合著了《Design Patterns - Elements o ...
  • 派生類繼承了基類除構造函數和析構函數外的所有數據成員和函數成員。派生類和基類存在一種特殊關係:派生類是一種基類,具有基類的所有功能。面向對象的程式設計利用派生類和基類之間的特殊關係,常常將派生類對象當作基類對象使用,或者用基類來代表派生類,其目的是提高代碼可重用性。由於C++對數據類型一致性要求比較 ...
  • Spring框架是什麼? Spring 是於 2003 年興起的一個輕量級的 Java 開發框架,它是為瞭解決企業應用開發的複雜性而創建的。Spring 的核心是控制反轉(IoC)和麵向切麵編程(AOP)。Spring 是可以在 Java SE/EE 中使用的輕量級開源框架。 Spring 的主要作 ...
  • 1.橋接方法簡介 橋接方法是jdk1.5引入泛型後,為使java泛型方法生成的位元組碼與jdk1.5版本之前的位元組碼相容由編譯器自動生成的。 可用method.isBridge() 判斷method是否是橋接方法,在生成的位元組碼中會有flags標記 ACC_BRIDGE, ACC_SYNTHETIC ...
  • 利用Python實現壓縮一個文件夾 二、知識點 文件讀寫 基礎語法 字元串處理 迴圈遍歷 文件壓縮 三、代碼解析 導入系統包 import platform import os import zipfile # 我還給大家準備了這些資料:Python視頻教程、100本Python電子書、基礎、爬蟲、 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...