這些年我參與和主導過多款音視頻 SDK 的設計和開發,也服務過大大小小几十家 toB 客戶,其中,有一條深深的感悟:微信
一個 PaaS 技術中間件產品,不管它的服務端 & 內核設計和實現的多麼牛逼多麼漂亮,最終交付給客戶開發者的 SDK 纔是最最關鍵的要素和門面,它設計得好,即便背後有不足也能有必定程度上的彌補;它設計的爛,就幾乎廢棄掉了底層全部的努力,還會平添無數的無效加班和問題排障的投入。
本文關注一款優秀的 SDK 應該如何設計接口規格,以實現以下幾個目標: 閉包
這裏致敬 《Effective C++》的行文模式,以條款的形式來描述和示例個人我的思考和總結(以最近深度參與的 RTC SDK 接口設計爲例子)。app
// good case // 記得給出合理的默認值 class AudioProfile { int samplerate{44100}; int channels{1}; }; // 記得給出合理的默認值 class VideoProfile { int maxEncodeWidth{1280}; int maxEncodeHeight{720}; int maxEncodeFps{15}; }; // 能夠很好地進行擴展,好比 SystemProfile,ScreenProfile... class EngineProfile { AudioProfile audio; VideoProfile video; }; class RtcEngine { public: static RtcEngine* CreateRtcEngine(const EngineProfile& profile) = 0; }; // bad case // 1. 核心接口類 RtcEngine 的函數數量爆炸 // 2. 沒法約束業務方調用 API 的時間(可能在加入房間後或者某個不合適的時間去配置參數) // 3. 若是某個配置指望支持動態更新怎麼辦 ?一般配置是不建議頻繁動態更新的(會影響 SDK 內部行爲), // 若有必須,請顯式在 engine 提供 updateXXXX or switchXXX 接口 class RtcEngine { public: static RtcEngine* CreateRtcEngine() = 0; virtual void setAudioSampelerate(int samplerate) = 0; virtual void setAudioChannels(int channels) = 0; virtual void setVideoMaxEncodeResolution(int width, int height) = 0; virtual void setVideoMaxEncodeFps(int fps) = 0; };
// good case class RtcEngine { public: static int GetSdkVersion(); static void SetLogLevel(int loglevel); };
// good case typedef std::function<void(int code, string message)> Callback; class RtcEngine { public: // 客戶可及時在 callback 中處理事件,好比:改變 UI 狀態|提示錯誤|再次重試 virtual void Publish(Callback const& callback = nullptr) = 0; virtual void Subscribe(Callback const& callback = nullptr) = 0; }; // bad case class RtcEngine { public: class Listener { // 須要根據 code 來詳細判斷錯誤事件,且不必定能對得上哪一次 API 調用產生的錯誤 // 錯誤種類繁多,且跳出原來的邏輯,不少業務方會忽略在這裏處理一些關鍵錯誤 virtual void OnError(int code, string message) = 0; }; void SetListener(Listener * listener) { _listener = listener; } virtual void Publish() = 0; virtual void Subscribe() = 0; private: Listener * _listener; };
// bad case // EnalbeAudio 與其餘 API 接口並不 「正交」,組合起來容易用錯 // MuteLocalAudioStream(true) & MuteAllRemoteAudioStreams(true) 依賴了使用者先調用 EnalbeLocalAudio(true) class RtcEngine { public: // EnalbeLocalAudio + MuteLocalAudioStream + MuteRemoteAudioStream virtual void EnalbeAudio(bool enable) = 0; // 打開本地的音頻設備(麥克風 & 揚聲器) virtual void EnalbeLocalAudio(bool enable) = 0; // 發佈/取消發佈本地音頻流 virtual void MuteLocalAudioStream(bool mute) = 0; // 訂閱/取消訂閱遠端音頻流 virtual void MuteAllRemoteAudioStreams(bool mute) = 0; };
// good case class RtcUser { string userId; string metadata; }; class RtcEngineEventListenr { public: // 將來能夠很容易擴展 User 的信息和屬性 virtual void OnUserJoined(const RtcUser& user) = 0; }; // bad case class RtcEngineEventListenr { public: // 一旦接口提供出去後,將來關於 User 對象的一些擴展信息和屬性沒法添加 virtual void OnUserJoined(string userId, string metadata) = 0; };
客戶在面對 SDK 提供的 OnError 回調事件的時候,因爲錯誤種類特別多,他們每每不知道該如何應對和處理,建議有明確的文檔告知處理方案。另外,當 SDK 內部發生了必須銷燬對象退出頁面的事件時,建議給出獨立的 callback 函數讓客戶專門處理。異步
enum ExitReason { EXIT_REASON_FATAL_ERROR, // 未知的關鍵異常 EXIT_REASON_RECONNECT_FAILED, // 斷線後自動重連達到次數&時間上限 EXIT_REASON_ROOM_CLOSED, // 房間被關閉了 EXIT_REASON_KICK_OUT, // 被踢出房間了 }; class RtcEngineEventListenr { public: // 一些警告消息,不礙事,接着用 virtual void OnWarning(int code, const string &message) = 0; // 發生了必須銷燬 SDK 對象的事件,請關閉頁面 virtual void OnExit(ExitReason reason, const string &message) = 0; };
// bad case enum ClientRole { CLIENT_ROLE_BROADCASTER, // 主播,能夠推流也能夠拉流 CLIENT_ROLE_AUDIENCE // 觀衆,不能推流僅能夠拉流 }; class RtcEngine { public: // 須要明確的文檔介紹不一樣的 role 所對應的角色,以及 role 切換產生的行爲 // 該 API 與其餘的 API 不是 「正交」 的,好比:Publish virtual void SetClientRole(ClientRole& role) = 0; }; // good case // 建議在 examples 或者最佳實踐中,封裝多個 SDK 的原子接口,以達成上述 API 所起到的做用 class RoleManager { public: // 經過這種方式,客戶能夠顯式地感知到這個 API 背後的一系列的行爲動做 void SetClientRole(ClientRole& role) { // _engine->xxxxx1(); // _engine->xxxxx2(); // _engine->xxxxx3(); } private: RtcEngine * _engine; };
// good case class RtcUser { string userId; string metadata; bool audio{false}; // 是否打開而且發佈了音頻流 bool video{false}; // 是否打開而且發佈了視頻流 bool screen{false}; // 是否打開而且發佈了屏幕流 }; class RtcEngine { public: // 由 SDK 內部來保持用戶狀態(最準確實時),並提供明確的查詢 API // 而不是讓客戶在本身的代碼中 cache 狀態(很容易出現兩邊狀態不一致的問題) virtual list<RtcUser> GetUsers() = 0; virtual RtcUser GetUsers(const string& userId) = 0; };
class VideoProfile { public: // 提供能力的枚舉和配置結果,從而防止客戶覺得的配置跟實際的狀況不一致 bool IsHwEncodeSupported(); bool SetHwEncodeEnabled(bool enabled); // 提供能力的枚舉和配置結果,從而防止客戶覺得的配置跟實際的狀況不一致 int GetSupportedMaxEncodeWidth(); int GetSupportedMaxEncodeHeight(); bool SetMaxEncodeResolution(int width, int height); };
// good case // 某個代碼 repo 的目錄結構(固然,僅 Android 的包客戶可感知,C++ 的庫外部沒法感知目錄結構) // 建議全部的對外的 interface 頭文件都在根目錄下,而實現文件隱藏在內部文件夾中 // 合理的頭文件位置關係,可以幫助開發者本身 & 客戶準確地感知哪些是接口文件,哪些是內部文件 // 全部的對外的頭文件,不容許 include 內部的文件,不然存在頭文件污染問題 // 全部的接口 Class 命名都以統一的風格開頭,好比 RtcXXXX,回調都叫 XXXCallback 等等 src - base - audio - video - utils - metrics - rtc_types.h - rtc_engine.h - rtc_engine_event_listener.h
關於 SDK 的接口設計經驗就介紹到這裏了,每一個人都會有本身的風格和喜愛,這裏僅表明我我的的一些觀點和見解,歡迎留言討論或者來信 lujun.hust@gmail.com 交流,或者關注個人微信公衆號 @Jhuster 獲取後續更多的文章和資訊~~ide