camera2對攝像頭編碼h264

MediaCodec編碼攝像頭數據

前置:保存的一些成員變量

// 攝像頭開啟的 handler
private Handler cameraHandler;
// Camera session 會話 handler
private Handler sessionHandler;
//這里是個Context都行
private AppCompatActivity mActivity;
// 這個攝像頭所有需要顯示的 Surface,以TextureView創建的Surface在一開始就已經加進去了,如果用SurfaceView的話,注意管理好SurfaceView因為onstop,onStart創建和銷毀的Surface
private List<Surface> mViewSurfaces;
private CameraManager cameraManager;private String mCameraId;
private CameraDevice mDevice;
//Camera 配置信息
private CameraCharacteristics mCharacteristics;
private CameraCaptureSession mSession;// 編碼輸入的Surface,由MediaCodec創建
private Surface mStreamSurface;
// 攝像頭選擇的輸出大小
private Size mSize;//編碼器
private MediaCodec mMediaCodec;

一、選擇要打開的攝像頭,并保存對應配置參數。

  1. 獲取打開的攝像頭 ID,以及獲取對應參數配置.
    try {
    String[] cameraIdList = cameraManager.getCameraIdList();
    // 遍歷攝像頭,獲取第一個可用的攝像頭,多個攝像頭暫不播放
    for (String id : cameraIdList) {
    CameraCharacteristics cameraCharacteristics = cameraManager.getCameraCharacteristics(id);
    // 獲取該攝像頭的說明數據
    int[] ints = cameraCharacteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
    if (ints != null) {
    Arrays.stream(ints).filter(value -> value == CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE).close();
    if (ints.length == 0) {
    Log.d(TAG, “the camera has not basic supports.”);
    continue;
    }
    mCameraId = id;
    mCharacteristics = cameraCharacteristics;
    // 由于目前無具體需求,使用第一個可連接的攝像頭進行顯示,選完就返回,有需要的獲取配置參數自行判定
    break;
    }
    }
    } catch (CameraAccessException e) {
    throw new RuntimeException(e);
    }

    說明:CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE 只是為了確定這個 Camera可用而已。

  2. 獲取攝像頭的所有的輸出尺寸,選一個合適的
    //獲取配置參數
    StreamConfigurationMap configurationMap = mCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
    //拿到對應 ImageFormat.YUV_420_888 所有可輸出的尺寸,一般比較多,1280x720,1920x1080等等一大堆,傳參有點重要下面說明會說.
    Stream stream = Arrays.stream(configurationMap.getOutputSizes(ImageFormat.YUV_420_888));
    //這里我選了寬高相乘最大的
    Optional max = stream
    .max(Comparator.comparingInt(item -> item.getWidth() * item.getHeight()));
    stream.close();
    //把輸出尺寸保存起來,后面創建編碼器要用,其實就是視頻的分辨率
    max.ifPresent(value -> mSize = value);

3. 比較重要的參數configurationMap.getOutputSizes(ImageFormat.YUV_420_888)傳參為什么是ImageFormat.YUV_420_888說明 比較重要的是,這個值不是隨便設置的,是通過獲取設備參數之后選的,只要是設備參數里有的都能用,但是沒有的值,用了可是會出錯的。

獲取Camera圖像格式的方法:
int[] outputFormats = configurationMap.getOutputFormats();

遍歷打印一下,選一個就行,但是盡量按照參數說明搭配需求去選。

二、創建編碼器

  1. 通過步驟一拿到的輸出尺寸,配置編碼格式.(只設置了必要的幾個參數,其他的自選加入)

     //創建編碼格式MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", mSize.getWidth(), mSize.getHeight());//設置碼率mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, (int) (mSize.getHeight() * mSize.getWidth() * 0.2));//設置幀率60mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 60);//設置顏色格式,因為這里用的自動編碼,就不用yuv420自己手動編了,直接用surfacemediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);//設置關鍵幀產生速度 1,單位是(幀/秒)mediaFormat.setFloat(MediaFormat.KEY_I_FRAME_INTERVAL, 1f);//攝像頭編碼角度會出現90度的偏差,設置正向旋轉90顯示正常效果,你要喜歡歪頭90看其實也行mediaFormat.setInteger(MediaFormat.KEY_ROTATION, 90);Log.d(TAG, "MediaCodec format: " + mediaFormat);
    

說明:
(1) 編碼格式創建中,video/avc 表示編碼為 h264 編碼,編碼其實就是為了降低視頻大小.

(2) 碼率設置中,后面的乘以0.2只是為了降低碼率,不然太清晰了。。。正常的大多數是乘以5,這樣看著清晰多,當然大小也會增加.

(3) 幀率設置為24以上就行,不然就是PPT。

(4) 關鍵幀產生速度SDK25之后可以設為float類型,關鍵幀就是 i 幀,設置之后只是說盡量按這個速度產生,肯定不會一模一樣的,0或者復數就是希望沒幀都是關鍵幀

  1. 創建編碼器
    try {
    //按照“video/avc”創建編碼器,硬編碼是基于硬件的,廠商不同會有不一樣的硬編碼實現,編碼是Encode,別用成Decode解碼器
    mMediaCodec = MediaCodec.createEncoderByType(“video/avc”);
    } catch (IOException e) {
    e.printStackTrace();
    }
    //配置編碼器,MediaCodec狀態機就不說了,這里只提使用。參數4也是說明是編碼器,和創建時不一致也會報錯
    mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    //創建輸入Surface,和MediaFormat設置的KEY_COLOR_FORMAT參數是有聯系的,format那里設錯了就不能用自動擋的surface,且createInputSurface()方法必須在configure()之后,start()之前調用
    mStreamSurface = mMediaCodec.createInputSurface();
    //開啟編碼
    mMediaCodec.start();

說明:四個點
(1) MediaCodec.createEncoderByType(“video/avc”),創建編碼器,MediaCodec.createDecoderByType(“video/avc”)是創建解碼器的。byName那個的話想用得先獲取硬件已實現的編碼器的名字才能調,雖然不麻煩,但是懶得敲,而且因為硬件廠商實現不同,兼容性還得考慮。

(2) configure()很容易就報錯,參數4編解碼值是不一樣的,以及MediaFormat的設置,如果和攝像頭、手機不支持也會報錯,所以前面寫的那些代碼就是為了拿的,不然隨便寫一個就行了。

(3) MediaCodec.createInputSuface()方法必須在 configure()配置編碼之后,start()開始編碼之前調用,點進實現也有說明,出現其實也挺好解決,你也可以用mMediaCodec.setInputSurface(創建的常顯Surface)去用自己的Surface,沒有常顯Surface的話可以靜態方法MediaCodec.createPersistentInputSurface()創建一個常顯Surface.

(4) 在 start()開啟編碼之后,MediaFormat參數格式其實是可以改的,只是需要用Bundle,通過key-value的方式設值,然后mMediaCodec.setParameters(bundle)去動態設置。

三、操作攝像頭

  1. 打開攝像頭預覽

攝像頭 ID之前已經拿過了,所以直接open就行了。
cameraManager.openCamera(cameraId, new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
Log.d(TAG, "onOpened, camera: " + camera);
mDevice = camera;

            // Camera 打開之后,把創建的解碼器 Surface 加入顯示隊列中addSurface(mStreamSurface);//創建請求CaptureRequest request = createCaptureRequest(mDevice, mViewSurfaces);//創建輸出配置項List<OutputConfiguration> outputConfigurations = createOutputConfig(mViewSurfaces);try {SessionConfiguration config = new SessionConfiguration(SessionConfiguration.SESSION_REGULAR,outputConfigurations,new ThreadPoolExecutor(3, 5, 15, TimeUnit.SECONDS, new ArrayBlockingQueue<>(25)),new CameraCaptureSession.StateCallback() {@Overridepublic void onConfigured(@NonNull CameraCaptureSession session) {mSession = session;try {//到這個回調,攝像頭就可以正常預覽了,編碼器也會輸出編碼數據到輸出隊列了mSession.setRepeatingRequest(request, null, sessionHandler);} catch (CameraAccessException e) {throw new RuntimeException(e);}}@Overridepublic void onConfigureFailed(@NonNull CameraCaptureSession session) {Log.e(TAG, "onConfigureFailed, camera: " + session);if (mSession != null) mSession.close();if (mDevice != null) mDevice.close();mDevice = null;}});mDevice.createCaptureSession(config);} catch (CameraAccessException e) {error();}}@Overridepublic void onDisconnected(@NonNull CameraDevice camera) {Log.e(TAG, "onDisconnected, camera: " + camera);}@Overridepublic void onError(@NonNull CameraDevice camera, int error) {Log.e(TAG, "onError, camera: " + camera + " ,error : " + error);}}, cameraHandler);
  1. 創建CaptureRequest請求的函數(因為涉及到多個Surface,就單獨創建了)

    private CaptureRequest createCaptureRequest(CameraDevice cameraDevice, List surfaces) {
    CaptureRequest.Builder captureRequest;
    try {
    //簡單預覽模式創建,有要求可以根據api去選video這些
    captureRequest = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
    //聚焦模式,不設置也可以
    captureRequest.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
    } catch (CameraAccessException e) {
    throw new RuntimeException(e);
    }
    if (surfaces != null) {
    //把每一個需要輸出攝像頭數據的Surface都加入
    for (Surface surface : surfaces) {
    if (surface == null) {
    continue;
    }
    captureRequest.addTarget(surface);
    }
    }
    return captureRequest.build();
    }

  2. 創建List的函數,Surface比較多,用的比較新的api去創建的session,老的api已經被標注遺棄了,所以單獨列個方法。

    private List createOutputConfig(List surfaces) {
    List configs = new ArrayList<>(surfaces.size());
    for (Surface surface : surfaces) {
    if (surface == null) {
    continue;
    }
    //SDK32,config.enableSurfaceSharing()開啟分享后addSurface()添加新的輸出面會有問題,所以就沒以一個Config去添加所有Surface
    OutputConfiguration config = new OutputConfiguration(surface);
    //就只創建,啥也不做,有要求可以自己加
    configs.add(config);
    }
    return configs;
    }

四、獲取編碼器的編碼數據

  1. 這部分網上挺多的,也不難.

    //這個是保存錄像數據的
    FileOutputStream outputStream;
    //每個關鍵幀的數據前面都需要添加spsPps的數據,不然無法解碼播放
    byte[] spsPps = null;

    boolean codecing = true;
    //編碼輸出信息的對象,賦值由MediaCodec做
    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();

         //當前可用的數據位置
    

    int outputBufferIndex;
    byte[] h264 = new byte[mCameraAdapter.getSize().getWidth() * mCameraAdapter.getSize().getHeight()];
    //死循環獲取,也可以對MediaCodec設置callback獲取
    while (codecing) {
    //獲取未來一段時間可用的輸出緩沖區,如果存在可用,返回值大于等于0,bufferInfo也會被賦值,最大等待時間是100000微秒,其實就是100毫秒
    outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 100000);
    Log.i(TAG, “dequeue output buffer outputBufferIndex=” + outputBufferIndex);

     //如果當前輸出緩沖區沒有可用的,返回負值,不同值含義不一樣,有需要做判定即可if (outputBufferIndex < 0) {                continue;}ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferIndex);Log.i(TAG, "Streaming   bufferInfo,flags: " + bufferInfo.flags+ ", size: " + bufferInfo.size+ ", presentationTimeUs: " + bufferInfo.presentationTimeUs+ ", offset: " + bufferInfo.offset);//調整數據位置,從offset開始。這樣我們一會兒讀取就不用傳offset偏差值了。 outputBuffer.position(bufferInfo.offset);//改完位置,那肯定要改極限位置吧,不然你數據不就少了數據末尾長度為offset的這一小部分?這兩步不做也可以,get的時候傳offset也一樣outputBuffer.limit(bufferInfo.offset + bufferInfo.size);//現在的spsPps還是空的,這是不能做寫入文件,保存之類操作的if (spsPps == null) {//創建寫入的文件,保存錄像try {File file = new File(mContext.getDataDir().getAbsolutePath() + "camera.h264");boolean newFile = file.createNewFile();outputStream = new FileOutputStream(file, true);} catch (Exception ignored) {}// sps pps獲取并保存,還有其他方式可以獲取,不過不一定有效,最好檢驗一下ByteBuffer sps = mMediaCodec.getOutputFormat().getByteBuffer("csd-0");ByteBuffer pps = mMediaCodec.getOutputFormat().getByteBuffer("csd-1");byte[] spsBites = new byte[sps.remaining()];byte[] ppsBites = new byte[pps.remaining()];sps.get(spsBites);pps.get(ppsBites);spsPps = new byte[spsBites.length + ppsBites.length];System.arraycopy(spsBites, 0, spsPps, 0, spsBites.length);System.arraycopy(ppsBites, 0, spsPps, spsBites.length, ppsBites.length);} else {//現在已經有spsPps數據了,可以開始了try {if (bufferInfo.size > h264.length) {h264 = new byte[bufferInfo.size];}h264 = new byte[bufferInfo.size];//獲取編碼數據outputBuffer.get(h264, 0, bufferInfo.size);//數據校驗一下,如果是關鍵幀需要在頭部加入spsPps,校驗完的trans,就是編碼好的h264編碼數據了byte[] trans = trans(h264, 0, bufferInfo.size, bufferInfo.flags);//保存錄像數據到文件中.暫定2G大小if (outputStream.getChannel().size() < (2L << 30)) {outputStream.write(trans);}//結束標志到達if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {pusher.stop();isStreaming = false;}} catch (Exception e) {e.printStackTrace();} finally {//釋放這個緩沖位置的數據,不釋放就一直在,MediaCodec數據滿了可不行mediaCodec.releaseOutputBuffer(outputBufferIndex, false);}}
    

    }

說明:
(1) 獲取編碼的 index,大于等于零才是有效的,不然不能用。

(2) 保存h264編碼數據之前,需要先獲取spsPps,不然關鍵幀之前沒有spsPps,解碼播放端也不加的話就沒法看了。

(3) 獲取數據注意offset,真正有效的數據是 bytes[offset] 到 bytes[offset+size]這個位置,至于你是get()的時候傳offset拿,還是possition()limit()后傳0拿,就看你心情了。

(4) 這個index的輸出緩沖區用完之后,記得釋放
(5) 這個錄像保存只是做個樣子,如果真是錄像,建議用 MediaMuxer 來做,直接傳判定 index 后 傳 buffinfo 就行,簡單方便。

  1. 這是校驗是不是關鍵幀的,很簡單明了

    private byte[] trans(byte[] h264, int offset, int length, int flags) {
    //關鍵幀判定
    if ((flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
    // 每個關鍵幀前面需要添加spsPps
    byte[] data = new byte[spsPps.length + length];
    System.arraycopy(spsPps, 0, data, 0, spsPps.length);
    System.arraycopy(h264, offset, data, spsPps.length, length);
    length = data.length;

         return data;} else {return h264;}
    

    }

五、播放保存的 h264 文件.

連上設備,我這邊設置的文件位置是 Context.getDataDir().getAbsolutePath()/camera.h264,真實目錄是 /data/data/包名/camera.h264,保存出來用vlc,或者ffmpeg命令ffplay播放就行了,親測:windows自帶的播放器無法解碼(笑)。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/207567.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/207567.shtml
英文地址,請注明出處:http://en.pswp.cn/news/207567.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

深入理解 Python 中的 eval 函數

更多資料獲取 &#x1f4da; 個人網站&#xff1a;ipengtao.com eval 是 Python 中一個強大而靈活的函數&#xff0c;它允許將字符串作為代碼執行。然而&#xff0c;由于其潛在的安全風險&#xff0c;使用時需要謹慎。本文將深入探討 eval 函數的各個方面&#xff0c;包括基本…

delphi/python 實現小紅書xhs用戶作品列表和圖片/視頻無水印解析

技術學習&#xff0c;請勿用與非法用途&#xff01;&#xff01;&#xff01; 成品圖用戶作品列表接口 /api/sns/web/v1/user_posted?num30&cursor&user_id642bf0850000000011022c4e&image_scenes http Get方式&#xff0c;請求頭需要帶上x-s x-t簽名驗證筆記明細…

直流負載箱的技術發展趨勢和創新有哪些?

直流負載箱廣泛應用于電子、通信、航空航天等領域&#xff0c;隨著科技的不斷發展&#xff0c;直流負載箱也在不斷創新和改進&#xff0c;直流負載箱在負載電流和電壓的測量方面要求高精度和高穩定性。未來的發展趨勢是提高負載箱的測量精度和穩定性&#xff0c;以滿足更高要求…

記錄一些好的文章

高效編寫可維護代碼&#xff1a; 如何高效編寫可維護代碼&#xff1f; | 菜鳥教程 (runoob.com)

計算平均分并輸出低于平均分的學生成績

從鍵盤上輸入若干&#xff08;<20&#xff09;個學生的成績&#xff0c;統計計算出平均成績&#xff0c;并輸出低于平均分的學生成績&#xff0c;用輸入負數結束輸入。 輸入格式: 在一行中輸入若干&#xff08;<20&#xff09;個學生的實型成績&#xff0c;用輸入負數結…

uniapp 使用 $emit和$on——$on中無法為data中的變量賦值

問題在于this的指向&#xff0c; 解決辦法是使用變量保存$on&#xff0c;其次再為data中的值賦值 以下是具體代碼&#xff1a; 1、html代碼&#xff1a; <view class"form_picker" click"selePositionFun()"><view class""><inp…

Git

第1章 Git 概述 Git 是一個免費的、開源的分布式版本控制系統&#xff0c;可以快速高效地處理從小型到大型的各種項目。 Git 易于學習&#xff0c;占地面積小&#xff0c;性能極快。 它具有廉價的本地庫&#xff0c;方便的暫存區域和多個工作流分支等特性。其性能優于 Subversi…

系統設計之數據庫

為您的項目選擇正確的數據庫是一項復雜的任務。許多數據庫選項都適合不同的用例&#xff0c;很快就會導致決策疲勞。 我們希望這份備忘單提供高級指導&#xff0c;以找到符合您項目需求的正確服務并避免潛在的陷阱。 注意&#xff1a;Google 關于其數據庫用例的文檔有限。盡管…

軟件測試卷王的自述,我難道真的很卷?

前言 前段時間去面試了一個公司&#xff0c;成功拿到了offer&#xff0c;薪資也從12k漲到了18k&#xff0c;對于工作都還沒兩年的我來說&#xff0c;還是比較滿意的&#xff0c;畢竟一些工作3、4年的可能還沒我高。 我可能就是大家說的卷王&#xff0c;感覺自己年輕&#xff…

北郵22級信通院數電:Verilog-FPGA(12)第十二周實驗(2)彩虹呼吸燈(bug已解決 更新至3.0)

北郵22信通一枚~ 跟隨課程進度更新北郵信通院數字系統設計的筆記、代碼和文章 持續關注作者 迎接數電實驗學習~ 獲取更多文章&#xff0c;請訪問專欄&#xff1a; 北郵22級信通院數電實驗_青山如墨雨如畫的博客-CSDN博客 目錄 一.代碼部分 1.1一些更新和講解 1.2改正后的…

解密HubSpot CMS Hub:構建引人入勝的企業網站!

在數字化時代&#xff0c;網站是企業與客戶互動的重要窗口。為了在競爭激烈的市場中脫穎而出&#xff0c;企業需要一個現代化、用戶友好且高度可定制的網站。而HubSpot CMS Hub作為一款領先的內容管理系統&#xff0c;為企業提供了獨特的優勢&#xff0c;讓網站建設變得更加輕松…

Private Set Intersection from Pseudorandom CorrelationGenerators 最快PSI!導覽解讀

目錄 一、概述 二、相關介紹 三、性能對比 四、技術細節 1.KKRT 2.Pseudorandom Correlation Generators 3.A New sVOLE-Based BaRK-OPRF 4.BaRK-OPRF 五、總結 參考文獻 一、概述 這篇文章的主要脈絡和核心思想是探討如何利用偽隨機相關生成器&#xff08;PCG&#…

【AI】以大廠PaaS為例,看人工智能技術方案服務能力的方向(2/2)

目錄 三、解決方案 3.1 人臉身份驗證 3.2 圖像審核&#xff08;暴恐、色情等&#xff09; 3.3 人臉會場簽到 3.4 機器人視覺 3.5 視頻審核 3.6 電商圖文詳情生成 3.7 智能客服 接上回&#xff1a; 【AI】以大廠PaaS為例&#xff0c;看人工智能技術方案服務能力的方向&…

Mybatis實用教程之XML實現動態sql

系列文章目錄 1、mybatis簡介及數據庫連接池 2、mybatis中selectOne的使用 3、mybatis簡單使用 4、mybatis中resultMap結果集的使用 Mybatis實用教程之XML實現動態sql 系列文章目錄前言1. 動態條件查詢2. 動態更新語句3. 動態插入語句4、其他標簽的使用 前言 當編寫 MyBatis 中…

力扣labuladong——一刷day67

提示&#xff1a;文章寫完后&#xff0c;目錄可以自動生成&#xff0c;如何生成可參考右邊的幫助文檔 文章目錄 前言一、力扣582.殺掉進程二、力扣536.從字符串生成二叉樹 前言 二叉樹的遞歸分為「遍歷」和「分解問題」兩種思維模式&#xff0c;這道題需要用到「遍歷」的思維模…

麒麟系統進入救援模式或者是crtl D界面排查方法

如出現以下圖片的情況可能需要修復磁盤&#xff1a; V10GFB-desktop&#xff1a; 開機后發現一致卡在此界面&#xff1a; 按esc鍵后有以下報錯信息說明在/etc/fstab里面編寫的外掛磁盤的命令有問題 解決方法如下&#xff1a;進入單用戶模式對/etc/fstab進行修改&#xff1a; …

springboot-mongodb-連接配置

文章目錄 配置Maven依賴URL格式單節點配置示例副本集&#xff08;含連接池配置&#xff09; 配置Maven依賴 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb</artifactId></dependenc…

智能優化算法應用:基于侏儒貓鼬算法無線傳感器網絡(WSN)覆蓋優化 - 附代碼

智能優化算法應用&#xff1a;基于侏儒貓鼬算法無線傳感器網絡(WSN)覆蓋優化 - 附代碼 文章目錄 智能優化算法應用&#xff1a;基于侏儒貓鼬算法無線傳感器網絡(WSN)覆蓋優化 - 附代碼1.無線傳感網絡節點模型2.覆蓋數學模型及分析3.侏儒貓鼬算法4.實驗參數設定5.算法結果6.參考…

facebook廣告運營技巧

在Facebook上進行廣告運營需要一定的技巧和策略。以下是一些建議&#xff1a; 明確目標&#xff1a;在創建廣告之前&#xff0c;你需要明確你的目標。這可能包括增加品牌知名度、提高網站流量、增加銷售等等。明確目標后&#xff0c;你可以將廣告與這些目標相結合&#xff0c;…

【五分鐘】熟悉python列表和元組的異同點【看這篇夠用!建議收藏】

引言 Python&#xff0c;是一種廣泛應用于數據科學、機器學習等領域的高級編程語言&#xff0c;支持多種豐富多樣的數據類型&#xff0c;其中包括列表和元組。盡管這兩種數據結構都可用于存儲多個值&#xff0c;但它們在功能和特性上存在著明顯的差異。在接下來的博客中&#…