From 20906a8b162cfe01833622d4839c19d28eaef3a9 Mon Sep 17 00:00:00 2001 From: ZmoleCristian Date: Wed, 12 Jun 2024 12:08:45 +0300 Subject: [PATCH] experimental room info access --- Cargo.lock | 2 +- FUNDING.yml | 3 +- README.md | 96 ++- src/core/live_client.rs | 10 +- src/core/live_client_http.rs | 65 +- src/core/live_client_websocket.rs | 75 ++- src/data/live_common.rs | 969 ++++++++++++++++++++++++++++++ src/errors.rs | 20 +- src/http/http_data.rs | 1 + src/main.rs | 44 +- 10 files changed, 1194 insertions(+), 91 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6510107..cfbe3f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1159,7 +1159,7 @@ dependencies = [ [[package]] name = "tiktoklive" -version = "0.0.11" +version = "0.0.12" dependencies = [ "bytes", "env_logger", diff --git a/FUNDING.yml b/FUNDING.yml index c61da06..28eb44a 100644 --- a/FUNDING.yml +++ b/FUNDING.yml @@ -1,2 +1,3 @@ patreon: jwdeveloper -custom: ["https://www.buymeacoffee.com/jwdev"] \ No newline at end of file +custom: ["https://www.buymeacoffee.com/jwdev"] +patreon: tragdate diff --git a/README.md b/README.md index a1389f1..a2523fc 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,9 @@ Do you prefer other programming languages? ```toml [dependencies] -tiktoklive = "0.0.12" +tiktoklive = "0.0.13" tokio = { version = "1.35.1", features = ["full"] } +serde_json = "1.0" log = "0.4" env_logger = "0.10.1" ``` @@ -57,11 +58,12 @@ env_logger = "0.10.1" ```rust use env_logger::{Builder, Env}; // Importing the logger builder and environment configuration use log::LevelFilter; // Importing log level filter +use log::{error, warn}; use std::time::Duration; // Importing Duration for timeout settings use tiktoklive::{ // Importing necessary modules and structs from tiktoklive crate core::live_client::TikTokLiveClient, - data::live_common::TikTokLiveSettings, + data::live_common::{ClientData, StreamData, TikTokLiveSettings}, errors::LibError, generated::events::TikTokLiveEvent, TikTokLive, @@ -84,19 +86,19 @@ async fn main() { // Match on the error type LibError::LiveStatusFieldMissing => { // Specific error case - println!( + warn!( "Failed to get live status (probably needs authenticated client): {}", e ); let auth_client = create_client_with_cookies(user_name); // Create an authenticated client if let Err(e) = auth_client.connect().await { // Attempt to connect the authenticated client - eprintln!("Error connecting to TikTok Live after retry: {}", e); + error!("Error connecting to TikTok Live after retry: {}", e); } } _ => { // General error case - eprintln!("Error connecting to TikTok Live: {}", e); + error!("Error connecting to TikTok Live: {}", e); } } } @@ -107,9 +109,35 @@ async fn main() { handle.await.expect("The spawned task has panicked"); // Await the spawned task to ensure it completes } -// Function to handle different types of TikTok live events -fn handle_event(_client: &TikTokLiveClient, event: &TikTokLiveEvent) { +fn handle_event(client: &TikTokLiveClient, event: &TikTokLiveEvent) { match event { + TikTokLiveEvent::OnConnected(..) => { + // This is an experimental and unstable feature + // Get room info from the client + let room_info = client.get_room_info(); + // Parse the room info + let client_data: ClientData = serde_json::from_str(room_info).unwrap(); + // Parse the stream data + let stream_data: StreamData = serde_json::from_str( + &client_data + .data + .stream_url + .live_core_sdk_data + .pull_data + .stream_data, + ) + .unwrap(); + // Get the video URL for the low definition stream with fallback to the high definition stream in a flv format + let video_url = stream_data + .data + .ld + .map(|ld| ld.main.flv) + .or_else(|| stream_data.data.sd.map(|sd| sd.main.flv)) + .or_else(|| stream_data.data.origin.map(|origin| origin.main.flv)) + .expect("None of the stream types set"); + println!("room info: {}", video_url); + } + // Match on the event type TikTokLiveEvent::OnMember(join_event) => { // Handle member join event @@ -157,11 +185,12 @@ fn configure(settings: &mut TikTokLiveSettings) { // Function to configure the TikTok live settings with cookies for authentication fn configure_with_cookies(settings: &mut TikTokLiveSettings) { settings.http_data.time_out = Duration::from_secs(12); // Set HTTP timeout to 12 seconds - let contents = "YOUR_COOKIES"; // Placeholder for cookies + let contents = ""; // Placeholder for cookies settings .http_data .headers - .insert("Cookie".to_string(), contents.to_string()); // Insert cookies into HTTP headers + .insert("Cookie".to_string(), contents.to_string()); + // Insert cookies into HTTP headers } // Function to create a TikTok live client for the given username @@ -181,6 +210,55 @@ fn create_client_with_cookies(user_name: &str) -> TikTokLiveClient { } ``` +## Library errors table + +You can catch errors on events with + +```rust +use tiktoklive::LibError; + +if let Err(e) = client.connect().await { + match e { + LibError::UserFieldMissing => { + println!("User field is missing"); + } + _ => { + eprintln!("Error connecting to TikTok Live: {}", e); + } + } +} +``` + +| Error type | Description | +| --- | --- | +| RoomIDFieldMissing | Room ID field is missing, contact developer | +| UserFieldMissing | User field is missing | +| UserDataFieldMissing | User data field is missing | +| LiveDataFieldMissing | Live data field is missing | +| JsonParseError | Error parsing JSON | +| UserMessageFieldMissing | User message field is missing | +| ParamsError | Params error | +| UserStatusFieldMissing | User status field is missing | +| LiveStatusFieldMissing | Live status field is missing | +| TitleFieldMissing | Title field is missing | +| UserCountFieldMissing | User count field is missing | +| StatsFieldMissing | Stats field is missing | +| LikeCountFieldMissing | Like count is missing | +| TotalUserFieldMissing | Total user field is missing | +| LiveRoomFieldMissing | Live room field is missing | +| StartTimeFieldMissing | Start time field is missing | +| UserNotFound | User not found | +| HostNotOnline | Live stream for host is not online!, current status HostOffline | +| InvalidHost | Invalid host in WebSocket URL | +| WebSocketConnectFailed | Failed to connect to WebSocket | +| PushFrameParseError | Unable to read push frame | +| WebcastResponseParseError | Unable to read webcast response | +| AckPacketSendError | Unable to send ack packet | +| HttpRequestFailed | HTTP request failed | +| UrlSigningFailed | URL signing failed | +| HeaderNotReceived | Header was not received | +| BytesParseError | Unable to parse bytes to Push Frame | + ## Contributing diff --git a/src/core/live_client.rs b/src/core/live_client.rs index c18d407..281a731 100644 --- a/src/core/live_client.rs +++ b/src/core/live_client.rs @@ -39,7 +39,7 @@ impl TikTokLiveClient { } } - pub async fn connect(self) -> Result<(), LibError> { + pub async fn connect(mut self) -> Result<(), LibError> { if *(self.room_info.connection_state.lock().unwrap()) != DISCONNECTED { warn!("Client already connected!"); return Ok(()); @@ -63,6 +63,8 @@ impl TikTokLiveClient { room_id: room_id.clone(), }) .await?; + + self.room_info.client_data = response.json; if response.live_status != HostOnline { error!( "Live stream for host is not online!, current status {:?}", @@ -86,7 +88,7 @@ impl TikTokLiveClient { message_mapper: TikTokLiveMessageMapper {}, running: Arc::new(AtomicBool::new(false)), }; - ws.start(response, client_arc).await; + let _ = ws.start(response?, client_arc).await; Ok(()) } @@ -100,6 +102,10 @@ impl TikTokLiveClient { self.event_observer.publish(self, event); } + pub fn get_room_info(&self) -> &String { + &self.room_info.client_data + } + pub fn set_connection_state(&self, state: ConnectionState) { let mut data = self.room_info.connection_state.lock().unwrap(); *data = state; diff --git a/src/core/live_client_http.rs b/src/core/live_client_http.rs index e707485..a8e7a16 100644 --- a/src/core/live_client_http.rs +++ b/src/core/live_client_http.rs @@ -36,15 +36,10 @@ impl TikTokLiveHttpClient { .as_json() .await; - if option.is_none() { - panic!("Unable to get info about user ") - } - let json = option.unwrap(); - match map_live_user_data_response(json) { - Ok(response) => Ok(response), - Err(e) => Err(e), - } + let json = option.ok_or(LibError::HttpRequestFailed)?; + map_live_user_data_response(json).map_err(|e| e) } + pub async fn fetch_live_data( &self, request: LiveDataRequest, @@ -58,22 +53,15 @@ impl TikTokLiveHttpClient { .as_json() .await; - if option.is_none() { - panic!("Unable to get info about live room") - } - let json = option.unwrap(); - - match map_live_data_response(json) { - Ok(response) => Ok(response), - Err(e) => Err(e), - } + let json = option.ok_or(LibError::HttpRequestFailed)?; + map_live_data_response(json).map_err(|e| e) } pub async fn fetch_live_connection_data( &self, request: LiveConnectionDataRequest, - ) -> LiveConnectionDataResponse { - //Preparing URL to sign + ) -> Result { + // Preparing URL to sign let url_to_sign = self .factory .request() @@ -81,7 +69,7 @@ impl TikTokLiveHttpClient { .with_param("room_id", request.room_id.as_str()) .as_url(); - //Signing URL + // Signing URL let option = self .factory .request() @@ -92,13 +80,10 @@ impl TikTokLiveHttpClient { .as_json() .await; - if option.is_none() { - panic!("Unable sign url {}", url_to_sign.as_str()) - } - let json = option.unwrap(); + let json = option.ok_or(LibError::UrlSigningFailed)?; let sign_server_response = map_sign_server_response(json); - //Getting credentials for connection to websocket + // Getting credentials for connection to websocket let response = self .factory .request() @@ -108,20 +93,23 @@ impl TikTokLiveHttpClient { .build_get_request() .send() .await - .unwrap(); + .map_err(|_| LibError::HttpRequestFailed)?; let optional_header = response.headers().get("set-cookie"); - - if optional_header.is_none() { - panic!("Header was not received not provided") - } - let header_value = optional_header.unwrap().to_str().unwrap().to_string(); - - let protocol_buffer_message = response.bytes().await.unwrap(); + let header_value = optional_header + .ok_or(LibError::HeaderNotReceived)? + .to_str() + .map_err(|_| LibError::HeaderNotReceived)? + .to_string(); + + let protocol_buffer_message = response + .bytes() + .await + .map_err(|_| LibError::BytesParseError)?; let webcast_response = WebcastResponse::parse_from_bytes(protocol_buffer_message.as_ref()) - .expect("Unable to parse bytes to Push Frame!"); + .map_err(|_| LibError::BytesParseError)?; - //preparing websocket url + // Preparing websocket URL let web_socket_url = self .factory .request() @@ -133,12 +121,11 @@ impl TikTokLiveHttpClient { .with_params(&webcast_response.routeParamsMap) .as_url(); - let url = url::Url::parse(web_socket_url.as_str()).unwrap(); - - return LiveConnectionDataResponse { + let url = url::Url::parse(web_socket_url.as_str()).map_err(|_| LibError::InvalidHost)?; + Ok(LiveConnectionDataResponse { web_socket_timeout: self.settings.http_data.time_out, web_socket_cookies: header_value, web_socket_url: url, - }; + }) } } diff --git a/src/core/live_client_websocket.rs b/src/core/live_client_websocket.rs index c77e884..ff85982 100644 --- a/src/core/live_client_websocket.rs +++ b/src/core/live_client_websocket.rs @@ -8,6 +8,7 @@ use tokio_tungstenite::tungstenite::handshake::client::Request; use crate::core::live_client::TikTokLiveClient; use crate::core::live_client_mapper::TikTokLiveMessageMapper; use crate::data::live_common::ConnectionState::CONNECTED; +use crate::errors::LibError; use crate::generated::events::{TikTokConnectedEvent, TikTokLiveEvent}; use crate::generated::messages::webcast::{WebcastPushFrame, WebcastResponse}; use crate::http::http_data::LiveConnectionDataResponse; @@ -25,11 +26,15 @@ impl TikTokLiveWebsocketClient { } } - pub async fn start(&self, response: LiveConnectionDataResponse, client: Arc) { + pub async fn start( + &self, + response: LiveConnectionDataResponse, + client: Arc, + ) -> Result<(), LibError> { let host = response .web_socket_url .host_str() - .expect("Invalid host in WebSocket URL"); + .ok_or(LibError::InvalidHost)?; let request = Request::builder() .method("GET") @@ -48,9 +53,9 @@ impl TikTokLiveWebsocketClient { .header("Cookie", response.web_socket_cookies) .header("Sec-Websocket-Version", "13") .body(()) - .unwrap(); + .map_err(|_| LibError::ParamsError)?; - let (mut socket, _) = connect(request).expect("Failed to connect"); + let (mut socket, _) = connect(request).map_err(|_| LibError::WebSocketConnectFailed)?; client.set_connection_state(CONNECTED); client.publish_event(TikTokLiveEvent::OnConnected(TikTokConnectedEvent {})); @@ -64,35 +69,45 @@ impl TikTokLiveWebsocketClient { while running.load(Ordering::SeqCst) { let optional_message = socket.read_message(); - if optional_message.is_err() { - continue; + if let Ok(message) = optional_message { + let buffer = message.into_data(); + + let mut push_frame = match WebcastPushFrame::parse_from_bytes(buffer.as_slice()) + { + Ok(frame) => frame, + Err(_) => continue, + }; + + let webcast_response = match WebcastResponse::parse_from_bytes( + push_frame.Payload.as_mut_slice(), + ) { + Ok(response) => response, + Err(_) => continue, + }; + + if webcast_response.needsAck { + let mut push_frame_ack = WebcastPushFrame::new(); + push_frame_ack.PayloadType = "ack".to_string(); + push_frame_ack.LogId = push_frame.LogId; + push_frame_ack.Payload = webcast_response.internalExt.clone().into_bytes(); + + let binary = match push_frame_ack.write_to_bytes() { + Ok(bytes) => bytes, + Err(_) => continue, + }; + + let message = tungstenite::protocol::Message::binary(binary); + if socket.write_message(message).is_err() { + continue; + } + } + + message_mapper.handle_webcast_response(webcast_response, client.as_ref()); } - let message = optional_message.unwrap(); - - let buffer = message.into_data(); - - let mut push_frame = WebcastPushFrame::parse_from_bytes(buffer.as_slice()) - .expect("Unable to read push frame"); - let webcast_response = - WebcastResponse::parse_from_bytes(push_frame.Payload.as_mut_slice()) - .expect("Unable to read webcast response"); - - if webcast_response.needsAck { - let mut push_frame_ack = WebcastPushFrame::new(); - push_frame_ack.PayloadType = "ack".to_string(); - push_frame_ack.LogId = push_frame.LogId; - push_frame_ack.Payload = webcast_response.internalExt.clone().into_bytes(); - - let binary = push_frame_ack.write_to_bytes().unwrap(); - let message = tungstenite::protocol::Message::binary(binary); - socket - .write_message(message) - .expect("Unable to send ack packet"); - } - - message_mapper.handle_webcast_response(webcast_response, client.as_ref()); } }); + + Ok(()) } pub fn stop(&self) { diff --git a/src/data/live_common.rs b/src/data/live_common.rs index 317e686..b75c453 100644 --- a/src/data/live_common.rs +++ b/src/data/live_common.rs @@ -1,7 +1,975 @@ +use serde_derive::Deserialize; +use serde_derive::Serialize; +use serde_json::Value; use std::collections::HashMap; use std::sync::Mutex; use std::time::Duration; +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ClientData { + pub data: Data, + pub extra: Extra2, + pub status_code: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Data { + #[serde(rename = "AnchorABMap")] + pub anchor_abmap: AnchorAbmap, + pub adjust_display_order: i64, + pub admin_ec_show_permission: AnchorAbmap, + pub admin_user_ids: Vec, + pub age_restricted: AgeRestricted, + pub allow_preview_time: i64, + pub anchor_scheduled_time_text: String, + pub anchor_share_text: String, + pub anchor_tab_type: i64, + pub answering_question_content: String, + pub app_id: i64, + pub audio_mute: i64, + pub auto_cover: i64, + pub book_end_time: i64, + pub book_time: i64, + pub business_live: i64, + pub challenge_info: String, + pub client_version: i64, + pub comment_has_text_emoji_emote: i64, + pub comment_name_mode: i64, + pub commerce_info: CommerceInfo, + pub common_label_list: String, + pub content_tag: String, + pub cover: Cover, + pub cpp_version: i64, + pub create_time: i64, + pub deco_list: Vec, + pub deprecated10: String, + pub deprecated11: String, + pub deprecated12: String, + pub deprecated13: String, + pub deprecated14: i64, + pub deprecated15: i64, + pub deprecated16: i64, + pub deprecated17: Vec, + pub deprecated18: i64, + pub deprecated19: String, + pub deprecated195: bool, + pub deprecated2: String, + pub deprecated20: i64, + pub deprecated21: bool, + pub deprecated22: i64, + pub deprecated23: String, + pub deprecated24: i64, + pub deprecated26: String, + pub deprecated28: String, + pub deprecated3: AnchorAbmap, + pub deprecated30: String, + pub deprecated31: bool, + pub deprecated32: String, + pub deprecated35: i64, + pub deprecated36: i64, + pub deprecated39: String, + pub deprecated4: i64, + pub deprecated41: i64, + pub deprecated43: bool, + pub deprecated44: i64, + pub deprecated5: bool, + pub deprecated6: String, + pub deprecated7: i64, + pub deprecated8: String, + pub deprecated9: String, + pub disable_preload_stream: bool, + pub disable_preview_sub_only: i64, + pub drawer_tab_position: String, + pub drop_comment_group: i64, + pub effect_info: Vec, + pub enable_server_drop: i64, + pub existed_commerce_goods: bool, + pub fansclub_msg_style: i64, + pub feed_room_label: Cover, + pub feed_room_labels: Vec, + pub filter_msg_rules: Vec, + pub finish_reason: i64, + pub finish_time: i64, + pub finish_url: String, + pub finish_url_v2: String, + pub follow_msg_style: i64, + pub forum_extra_data: String, + pub game_demo: i64, + pub game_tag: Vec, + pub gift_msg_style: i64, + pub gift_poll_vote_enabled: bool, + pub group_source: i64, + pub has_commerce_goods: bool, + pub has_more_history_comment: bool, + pub has_used_music: bool, + pub hashtag: Hashtag, + pub have_wishlist: bool, + pub history_comment_cursor: String, + pub history_comment_list: Vec, + pub hot_sentence_info: String, + pub id: i64, + pub id_str: String, + pub idc_region: String, + pub indicators: Vec, + pub interaction_question_version: i64, + pub introduction: String, + pub is_gated_room: bool, + pub is_replay: bool, + pub is_show_user_card_switch: bool, + pub last_ping_time: i64, + pub layout: i64, + pub like_count: i64, + pub link_mic: LinkMic, + pub linker_map: AnchorAbmap, + pub linkmic_layout: i64, + pub lite_user_not_visible: bool, + pub lite_user_visible: bool, + pub live_distribution: Vec, + pub live_id: i64, + pub live_reason: String, + pub live_room_mode: i64, + pub live_sub_only: i64, + pub live_sub_only_use_music: i64, + pub live_type_audio: bool, + pub live_type_linkmic: bool, + pub live_type_normal: bool, + pub live_type_sandbox: bool, + pub live_type_screenshot: bool, + pub live_type_social_live: bool, + pub live_type_third_party: bool, + pub living_room_attrs: LivingRoomAttrs, + pub lottery_finish_time: i64, + pub max_preview_time: i64, + pub mosaic_status: i64, + pub multi_stream_id: i64, + pub multi_stream_id_str: String, + pub multi_stream_scene: i64, + pub multi_stream_source: i64, + pub net_mode: i64, + pub os_type: i64, + pub owner: Owner, + pub owner_device_id: i64, + pub owner_device_id_str: String, + pub owner_user_id: i64, + pub owner_user_id_str: String, + pub paid_event: PaidEvent, + pub pico_live_type: i64, + pub polling_star_comment: bool, + pub pre_enter_time: i64, + pub preview_flow_tag: i64, + pub quota_config: AnchorAbmap, + pub rank_comment_groups: Vec, + pub ranklist_audience_type: i64, + pub regional_restricted: RegionalRestricted, + pub relation_tag: String, + pub replay: bool, + pub room_audit_status: i64, + pub room_auth: RoomAuth, + pub room_create_ab_param: String, + pub room_layout: i64, + pub room_pcu: i64, + pub room_sticker_list: Vec, + pub room_tabs: Vec, + pub room_tag: i64, + pub rtc_app_id: String, + pub scroll_config: String, + pub search_id: i64, + pub share_msg_style: i64, + pub share_url: String, + pub short_title: String, + pub short_touch_items: Vec, + pub show_star_comment_entrance: bool, + pub social_interaction: SocialInteraction, + pub start_time: i64, + pub stats: Stats, + pub status: i64, + pub sticker_list: Vec, + pub stream_id: i64, + pub stream_id_str: String, + pub stream_status: i64, + pub stream_url: StreamUrl, + pub stream_url_filtered_info: StreamUrlFilteredInfo, + pub support_quiz: i64, + pub title: String, + pub top_fans: Vec, + pub use_filter: bool, + pub user_count: i64, + pub user_share_text: String, + pub video_feed_tag: String, + pub webcast_comment_tcs: i64, + pub webcast_sdk_version: i64, + pub with_draw_something: bool, + pub with_ktv: bool, + pub with_linkmic: bool, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct AnchorAbmap {} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct AgeRestricted { + #[serde(rename = "AgeInterval")] + pub age_interval: i64, + pub restricted: bool, + pub source: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CommerceInfo { + pub commerce_permission: i64, + pub oec_live_enter_room_init_data: String, + pub product_num: i64, + pub use_async_load: bool, + pub use_new_promotion: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Cover { + pub avg_color: String, + pub height: i64, + pub image_type: i64, + pub is_animated: bool, + pub open_web_url: String, + pub uri: String, + pub url_list: Vec, + pub width: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct GameTag { + pub bundle_id: String, + pub full_name: String, + pub game_category: Vec, + pub hashtag_id: Vec, + pub hashtag_list: Vec, + pub id: i64, + pub is_new_game: bool, + pub landscape: i64, + pub package_name: String, + pub short_name: String, + pub show_name: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Hashtag { + pub id: i64, + pub image: Cover, + pub namespace: i64, + pub title: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct LinkMic { + pub audience_id_list: Vec, + pub battle_scores: Vec, + pub battle_settings: BattleSettings, + pub channel_id: i64, + pub followed_count: i64, + pub linked_user_list: Vec, + pub multi_live_enum: i64, + pub rival_anchor_id: i64, + pub show_user_list: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct BattleSettings { + pub battle_id: i64, + pub channel_id: i64, + pub duration: i64, + pub finished: i64, + pub match_type: i64, + pub start_time: i64, + pub start_time_ms: i64, + pub theme: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct LivingRoomAttrs { + pub admin_flag: i64, + pub rank: i64, + pub room_id: i64, + pub room_id_str: String, + pub silence_flag: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Owner { + pub allow_find_by_contacts: bool, + pub allow_others_download_video: bool, + pub allow_others_download_when_sharing_video: bool, + pub allow_share_show_profile: bool, + pub allow_show_in_gossip: bool, + pub allow_show_my_action: bool, + pub allow_strange_comment: bool, + pub allow_unfollower_comment: bool, + pub allow_use_linkmic: bool, + pub avatar_large: Cover, + pub avatar_medium: Cover, + pub avatar_thumb: Cover, + pub badge_image_list: Vec, + pub badge_list: Vec, + pub bg_img_url: String, + pub bio_description: String, + pub block_status: i64, + pub border_list: Vec, + pub comment_restrict: i64, + pub commerce_webcast_config_ids: Vec, + pub constellation: String, + pub create_time: i64, + pub deprecated1: i64, + pub deprecated12: i64, + pub deprecated13: i64, + pub deprecated15: i64, + pub deprecated16: bool, + pub deprecated17: bool, + pub deprecated18: String, + pub deprecated19: bool, + pub deprecated2: i64, + pub deprecated21: i64, + pub deprecated28: bool, + pub deprecated29: String, + pub deprecated3: i64, + pub deprecated4: i64, + pub deprecated5: String, + pub deprecated6: i64, + pub deprecated7: String, + pub deprecated8: i64, + pub disable_ichat: i64, + pub display_id: String, + pub enable_ichat_img: i64, + pub exp: i64, + pub fan_ticket_count: i64, + pub fold_stranger_chat: bool, + pub follow_info: FollowInfo, + pub follow_status: i64, + pub ichat_restrict_type: i64, + pub id: i64, + pub id_str: String, + pub is_block: bool, + pub is_follower: bool, + pub is_following: bool, + pub is_subscribe: bool, + pub link_mic_stats: i64, + pub media_badge_image_list: Vec, + pub mint_type_label: Vec, + pub modify_time: i64, + pub need_profile_guide: bool, + pub new_real_time_icons: Vec, + pub nickname: String, + pub own_room: OwnRoom, + pub pay_grade: PayGrade, + pub pay_score: i64, + pub pay_scores: i64, + pub push_comment_status: bool, + pub push_digg: bool, + pub push_follow: bool, + pub push_friend_action: bool, + pub push_ichat: bool, + pub push_status: bool, + pub push_video_post: bool, + pub push_video_recommend: bool, + pub real_time_icons: Vec, + pub scm_label: String, + pub sec_uid: String, + pub secret: i64, + pub share_qrcode_uri: String, + pub special_id: String, + pub status: i64, + pub ticket_count: i64, + pub top_fans: Vec, + pub top_vip_no: i64, + pub upcoming_event_list: Vec, + pub user_attr: UserAttr, + pub user_role: i64, + pub verified: bool, + pub verified_content: String, + pub verified_reason: String, + pub with_car_management_permission: bool, + pub with_commerce_permission: bool, + pub with_fusion_shop_entry: bool, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct BadgeList { + #[serde(rename = "OpenWebURL")] + pub open_web_url: String, + pub combine: Combine, + pub display: bool, + pub display_status: i64, + pub display_type: i64, + pub exhibition_type: i64, + pub greyed_by_client: i64, + pub is_customized: bool, + pub position: i64, + pub priority_type: i64, + pub privilege_log_extra: PrivilegeLogExtra, + pub scene_type: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Combine { + pub background: Background, + pub background_auto_mirrored: bool, + pub background_dark_mode: Background, + pub display_type: i64, + pub font_style: FontStyle, + pub icon: Cover, + pub icon_auto_mirrored: bool, + pub multi_guest_show_style: i64, + pub padding: Padding, + pub padding_new_font: Padding, + pub personal_card_show_style: i64, + pub profile_card_panel: ProfileCardPanel, + pub public_screen_show_style: i64, + pub ranklist_online_audience_show_style: i64, + pub str: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Background { + pub background_color_code: String, + pub border_color_code: String, + pub image: Image, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Image { + pub avg_color: String, + pub height: i64, + pub image_type: i64, + pub is_animated: bool, + pub open_web_url: String, + pub uri: String, + pub url_list: Vec, + pub width: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct FontStyle { + pub border_color: String, + pub font_color: String, + pub font_size: i64, + pub font_width: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Padding { + pub badge_width: i64, + pub horizontal_padding_rule: i64, + pub icon_bottom_padding: i64, + pub icon_top_padding: i64, + pub left_padding: i64, + pub middle_padding: i64, + pub right_padding: i64, + pub use_specific: bool, + pub vertical_padding_rule: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ProfileCardPanel { + pub badge_text_position: i64, + pub profile_content: ProfileContent, + pub projection_config: ProjectionConfig, + pub use_new_profile_card_style: bool, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ProfileContent { + pub icon_list: Vec, + pub use_content: bool, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ProjectionConfig { + pub icon: Image, + pub use_projection: bool, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PrivilegeLogExtra { + pub data_version: String, + pub level: String, + pub privilege_id: String, + pub privilege_order_id: String, + pub privilege_version: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct FollowInfo { + pub follow_status: i64, + pub follower_count: i64, + pub following_count: i64, + pub push_status: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct OwnRoom { + pub room_ids: Vec, + pub room_ids_str: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PayGrade { + pub deprecated20: i64, + pub deprecated22: i64, + pub deprecated23: i64, + pub deprecated24: i64, + pub deprecated25: i64, + pub deprecated26: i64, + pub grade_banner: String, + pub grade_describe: String, + pub grade_icon_list: Vec, + pub level: i64, + pub name: String, + pub next_name: String, + pub next_privileges: String, + pub score: i64, + pub screen_chat_type: i64, + pub upgrade_need_consume: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct UserAttr { + pub admin_permissions: AnchorAbmap, + pub is_admin: bool, + pub is_muted: bool, + pub is_super_admin: bool, + pub mute_duration: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PaidEvent { + pub event_id: i64, + pub paid_type: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RegionalRestricted { + pub block_list: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RoomAuth { + #[serde(rename = "Banner")] + pub banner: i64, + #[serde(rename = "BroadcastMessage")] + pub broadcast_message: i64, + #[serde(rename = "Chat")] + pub chat: bool, + #[serde(rename = "ChatL2")] + pub chat_l2: bool, + #[serde(rename = "ChatSubOnly")] + pub chat_sub_only: bool, + #[serde(rename = "CommercePermission")] + pub commerce_permission: i64, + #[serde(rename = "CommunityFlagged")] + pub community_flagged: bool, + #[serde(rename = "CommunityFlaggedReview")] + pub community_flagged_review: bool, + #[serde(rename = "CustomizableGiftPoll")] + pub customizable_gift_poll: i64, + #[serde(rename = "CustomizablePoll")] + pub customizable_poll: i64, + #[serde(rename = "Danmaku")] + pub danmaku: bool, + #[serde(rename = "Digg")] + pub digg: bool, + #[serde(rename = "DonationSticker")] + pub donation_sticker: i64, + #[serde(rename = "EnableFansLevel")] + pub enable_fans_level: bool, + #[serde(rename = "EventPromotion")] + pub event_promotion: i64, + #[serde(rename = "Explore")] + pub explore: bool, + #[serde(rename = "GameRankingSwitch")] + pub game_ranking_switch: i64, + #[serde(rename = "Gift")] + pub gift: bool, + #[serde(rename = "GiftAnchorMt")] + pub gift_anchor_mt: i64, + #[serde(rename = "GiftPoll")] + pub gift_poll: i64, + #[serde(rename = "GoldenEnvelope")] + pub golden_envelope: i64, + #[serde(rename = "GoldenEnvelopeActivity")] + pub golden_envelope_activity: i64, + #[serde(rename = "InteractionQuestion")] + pub interaction_question: bool, + #[serde(rename = "Landscape")] + pub landscape: i64, + #[serde(rename = "LandscapeChat")] + pub landscape_chat: i64, + #[serde(rename = "LuckMoney")] + pub luck_money: bool, + #[serde(rename = "MultiEnableReserve")] + pub multi_enable_reserve: bool, + #[serde(rename = "Pictionary")] + pub pictionary: i64, + #[serde(rename = "PictionaryBubble")] + pub pictionary_bubble: i64, + #[serde(rename = "PictionaryPermission")] + pub pictionary_permission: i64, + #[serde(rename = "Poll")] + pub poll: i64, + #[serde(rename = "Promote")] + pub promote: bool, + #[serde(rename = "PromoteOther")] + pub promote_other: i64, + #[serde(rename = "Props")] + pub props: bool, + #[serde(rename = "PublicScreen")] + pub public_screen: i64, + #[serde(rename = "QuickChat")] + pub quick_chat: i64, + #[serde(rename = "Rank")] + pub rank: i64, + #[serde(rename = "RankingChangeAlterSwitch")] + pub ranking_change_alter_switch: i64, + #[serde(rename = "RoomContributor")] + pub room_contributor: bool, + #[serde(rename = "SecretRoom")] + pub secret_room: i64, + #[serde(rename = "Share")] + pub share: bool, + #[serde(rename = "ShareEffect")] + pub share_effect: i64, + #[serde(rename = "ShoppingRanking")] + pub shopping_ranking: i64, + #[serde(rename = "SpamComments")] + pub spam_comments: bool, + #[serde(rename = "UserCard")] + pub user_card: bool, + #[serde(rename = "UserCount")] + pub user_count: i64, + #[serde(rename = "Viewers")] + pub viewers: bool, + pub comment_tray_status: i64, + pub credit_entrance_for_audience: bool, + pub deprecated1: bool, + pub deprecated118: Vec, + pub deprecated119: i64, + pub deprecated2: i64, + pub deprecated3: i64, + pub deprecated4: i64, + pub deprecated5: i64, + pub deprecated6: i64, + pub deprecated7: i64, + pub deprecated8: i64, + pub deprecated9: i64, + pub game_guess_permission: bool, + pub guess_entrance_for_host: bool, + pub show_credit_widget: bool, + pub transaction_history: i64, + pub use_user_pv: bool, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SocialInteraction { + pub linkmic_scene_linker: AnchorAbmap, + pub multi_live: MultiLive, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct MultiLive { + pub audience_shared_invitee_panel_type: i64, + pub host_gifter_linkmic_enum: i64, + pub host_multi_guest_dev_mode: i64, + pub linkmic_service_version: i64, + pub try_open_multi_guest_when_create_room: bool, + pub user_settings: UserSettings, + pub viewer_gifter_linkmic_enum: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct UserSettings { + pub applier_sort_gift_score_threshold: i64, + pub applier_sort_setting: i64, + pub multi_live_apply_permission: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Stats { + pub deprecated1: i64, + pub deprecated2: String, + pub digg_count: i64, + pub enter_count: i64, + pub fan_ticket: i64, + pub follow_count: i64, + pub gift_uv_count: i64, + pub id: i64, + pub id_str: String, + pub like_count: i64, + pub replay_fan_ticket: i64, + pub replay_viewers: i64, + pub share_count: i64, + pub total_user: i64, + pub total_user_desp: String, + pub watermelon: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct StreamUrl { + pub candidate_resolution: Vec, + pub complete_push_urls: Vec, + pub default_resolution: String, + pub extra: Extra, + pub flv_pull_url: FlvPullUrl, + pub flv_pull_url_params: FlvPullUrl, + pub hls_pull_url: String, + pub hls_pull_url_map: AnchorAbmap, + pub hls_pull_url_params: String, + pub id: i64, + pub id_str: String, + pub live_core_sdk_data: LiveCoreSdkData, + pub provider: i64, + pub push_resolution: String, + pub push_urls: Vec, + pub resolution_name: ResolutionName, + pub rtmp_pull_url: String, + pub rtmp_pull_url_params: String, + pub rtmp_push_url: String, + pub rtmp_push_url_params: String, + pub stream_app_id: i64, + pub stream_control_type: i64, + pub stream_delay_ms: i64, + pub vr_type: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Extra { + pub anchor_interact_profile: i64, + pub audience_interact_profile: i64, + pub bframe_enable: bool, + pub bitrate_adapt_strategy: i64, + pub bytevc1_enable: bool, + pub default_bitrate: i64, + pub deprecated1: bool, + pub fps: i64, + pub gop_sec: i64, + pub hardware_encode: bool, + pub height: i64, + pub max_bitrate: i64, + pub min_bitrate: i64, + pub roi: bool, + pub sw_roi: bool, + pub video_profile: i64, + pub width: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct FlvPullUrl { + #[serde(rename = "HD1")] + pub hd1: String, + #[serde(rename = "SD1")] + pub sd1: String, + #[serde(rename = "SD2")] + pub sd2: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct LiveCoreSdkData { + pub pull_data: PullData, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PullData { + pub options: Options, + pub stream_data: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Options { + pub default_quality: DefaultQuality, + pub qualities: Vec, + pub show_quality_button: bool, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DefaultQuality { + pub icon_type: i64, + pub level: i64, + pub name: String, + pub resolution: String, + pub sdk_key: String, + pub v_codec: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ResolutionName { + #[serde(rename = "AUTO")] + pub auto: String, + #[serde(rename = "FULL_HD1")] + pub full_hd1: String, + #[serde(rename = "HD1")] + pub hd1: String, + #[serde(rename = "ORIGION")] + pub origion: String, + #[serde(rename = "SD1")] + pub sd1: String, + #[serde(rename = "SD2")] + pub sd2: String, + pub pm_mt_video_1080p60: String, + pub pm_mt_video_720p60: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct StreamUrlFilteredInfo { + pub is_gated_room: bool, + pub is_paid_event: bool, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct TopFan { + pub fan_ticket: i64, + pub user: User, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct User { + pub allow_find_by_contacts: bool, + pub allow_others_download_video: bool, + pub allow_others_download_when_sharing_video: bool, + pub allow_share_show_profile: bool, + pub allow_show_in_gossip: bool, + pub allow_show_my_action: bool, + pub allow_strange_comment: bool, + pub allow_unfollower_comment: bool, + pub allow_use_linkmic: bool, + pub avatar_large: Cover, + pub avatar_medium: Cover, + pub avatar_thumb: Cover, + pub badge_image_list: Vec, + pub badge_list: Vec, + pub bg_img_url: String, + pub bio_description: String, + pub block_status: i64, + pub border_list: Vec, + pub comment_restrict: i64, + pub commerce_webcast_config_ids: Vec, + pub constellation: String, + pub create_time: i64, + pub deprecated1: i64, + pub deprecated12: i64, + pub deprecated13: i64, + pub deprecated15: i64, + pub deprecated16: bool, + pub deprecated17: bool, + pub deprecated18: String, + pub deprecated19: bool, + pub deprecated2: i64, + pub deprecated21: i64, + pub deprecated28: bool, + pub deprecated29: String, + pub deprecated3: i64, + pub deprecated4: i64, + pub deprecated5: String, + pub deprecated6: i64, + pub deprecated7: String, + pub deprecated8: i64, + pub disable_ichat: i64, + pub display_id: String, + pub enable_ichat_img: i64, + pub exp: i64, + pub fan_ticket_count: i64, + pub fold_stranger_chat: bool, + pub follow_info: FollowInfo, + pub follow_status: i64, + pub ichat_restrict_type: i64, + pub id: i64, + pub id_str: String, + pub is_block: bool, + pub is_follower: bool, + pub is_following: bool, + pub is_subscribe: bool, + pub link_mic_stats: i64, + pub media_badge_image_list: Vec, + pub mint_type_label: Vec, + pub modify_time: i64, + pub need_profile_guide: bool, + pub new_real_time_icons: Vec, + pub nickname: String, + pub pay_grade: PayGrade, + pub pay_score: i64, + pub pay_scores: i64, + pub push_comment_status: bool, + pub push_digg: bool, + pub push_follow: bool, + pub push_friend_action: bool, + pub push_ichat: bool, + pub push_status: bool, + pub push_video_post: bool, + pub push_video_recommend: bool, + pub real_time_icons: Vec, + pub scm_label: String, + pub sec_uid: String, + pub secret: i64, + pub share_qrcode_uri: String, + pub special_id: String, + pub status: i64, + pub ticket_count: i64, + pub top_fans: Vec, + pub top_vip_no: i64, + pub upcoming_event_list: Vec, + pub user_attr: UserAttr, + pub user_role: i64, + pub verified: bool, + pub verified_content: String, + pub verified_reason: String, + pub with_car_management_permission: bool, + pub with_commerce_permission: bool, + pub with_fusion_shop_entry: bool, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Extra2 { + pub now: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct StreamData { + pub common: Common, + pub data: NestedData, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Common { + pub peer_anchor_level: i64, + pub session_id: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct NestedData { + pub ao: Option, + pub hd: Option, + pub ld: Option, + pub origin: Option, + pub sd: Option, + pub uhd: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Ao { + pub main: Main, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Main { + pub cmaf: String, + pub dash: String, + pub flv: String, + pub hls: String, + pub lls: String, + pub tile: String, + pub tsl: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Demotion { + #[serde(rename = "StallCount")] + pub stall_count: i64, +} + #[derive(Clone)] pub struct TikTokLiveSettings { pub host_name: String, @@ -22,6 +990,7 @@ pub struct HttpData { #[derive(Default)] pub struct TikTokLiveInfo { pub room_id: String, + pub client_data: String, pub likes: i32, pub viewers: i32, pub total_viewers: i32, diff --git a/src/errors.rs b/src/errors.rs index a4e5557..5646dda 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -20,6 +20,15 @@ pub enum LibError { StartTimeFieldMissing, UserNotFound, HostNotOnline, + InvalidHost, + WebSocketConnectFailed, + PushFrameParseError, + WebcastResponseParseError, + AckPacketSendError, + HttpRequestFailed, + UrlSigningFailed, + HeaderNotReceived, + BytesParseError, } impl fmt::Display for LibError { @@ -39,7 +48,7 @@ impl fmt::Display for LibError { LibError::TitleFieldMissing => write!(f, "Title field is missing"), LibError::UserCountFieldMissing => write!(f, "User count field is missing"), LibError::StatsFieldMissing => write!(f, "Stats field is missing"), - LibError::LikeCountFieldMissing => write!(f, "Like count field is missing"), + LibError::LikeCountFieldMissing => write!(f, "Like count is missing"), LibError::TotalUserFieldMissing => write!(f, "Total user field is missing"), LibError::LiveRoomFieldMissing => write!(f, "Live room field is missing"), LibError::StartTimeFieldMissing => write!(f, "Start time field is missing"), @@ -48,6 +57,15 @@ impl fmt::Display for LibError { f, "Live stream for host is not online!, current status HostOffline" ), + LibError::InvalidHost => write!(f, "Invalid host in WebSocket URL"), + LibError::WebSocketConnectFailed => write!(f, "Failed to connect to WebSocket"), + LibError::PushFrameParseError => write!(f, "Unable to read push frame"), + LibError::WebcastResponseParseError => write!(f, "Unable to read webcast response"), + LibError::AckPacketSendError => write!(f, "Unable to send ack packet"), + LibError::HttpRequestFailed => write!(f, "HTTP request failed"), + LibError::UrlSigningFailed => write!(f, "URL signing failed"), + LibError::HeaderNotReceived => write!(f, "Header was not received"), + LibError::BytesParseError => write!(f, "Unable to parse bytes to Push Frame!"), } } } diff --git a/src/http/http_data.rs b/src/http/http_data.rs index d6b6ad5..626c691 100644 --- a/src/http/http_data.rs +++ b/src/http/http_data.rs @@ -17,6 +17,7 @@ pub struct LiveDataRequest { pub room_id: String, } +#[derive(Debug, PartialEq)] pub struct LiveDataResponse { pub json: String, pub live_status: LiveStatus, diff --git a/src/main.rs b/src/main.rs index ef22d95..88fa514 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,11 @@ use env_logger::{Builder, Env}; // Importing the logger builder and environment configuration use log::LevelFilter; // Importing log level filter +use log::{error, warn}; use std::time::Duration; // Importing Duration for timeout settings use tiktoklive::{ // Importing necessary modules and structs from tiktoklive crate core::live_client::TikTokLiveClient, - data::live_common::TikTokLiveSettings, + data::live_common::{ClientData, StreamData, TikTokLiveSettings}, errors::LibError, generated::events::TikTokLiveEvent, TikTokLive, @@ -27,19 +28,19 @@ async fn main() { // Match on the error type LibError::LiveStatusFieldMissing => { // Specific error case - println!( + warn!( "Failed to get live status (probably needs authenticated client): {}", e ); let auth_client = create_client_with_cookies(user_name); // Create an authenticated client if let Err(e) = auth_client.connect().await { // Attempt to connect the authenticated client - eprintln!("Error connecting to TikTok Live after retry: {}", e); + error!("Error connecting to TikTok Live after retry: {}", e); } } _ => { // General error case - eprintln!("Error connecting to TikTok Live: {}", e); + error!("Error connecting to TikTok Live: {}", e); } } } @@ -50,9 +51,35 @@ async fn main() { handle.await.expect("The spawned task has panicked"); // Await the spawned task to ensure it completes } -// Function to handle different types of TikTok live events -fn handle_event(_client: &TikTokLiveClient, event: &TikTokLiveEvent) { +fn handle_event(client: &TikTokLiveClient, event: &TikTokLiveEvent) { match event { + TikTokLiveEvent::OnConnected(..) => { + // This is an experimental and unstable feature + // Get room info from the client + let room_info = client.get_room_info(); + // Parse the room info + let client_data: ClientData = serde_json::from_str(room_info).unwrap(); + // Parse the stream data + let stream_data: StreamData = serde_json::from_str( + &client_data + .data + .stream_url + .live_core_sdk_data + .pull_data + .stream_data, + ) + .unwrap(); + // Get the video URL for the low definition stream with fallback to the high definition stream in a flv format + let video_url = stream_data + .data + .ld + .map(|ld| ld.main.flv) + .or_else(|| stream_data.data.sd.map(|sd| sd.main.flv)) + .or_else(|| stream_data.data.origin.map(|origin| origin.main.flv)) + .expect("None of the stream types set"); + println!("room info: {}", video_url); + } + // Match on the event type TikTokLiveEvent::OnMember(join_event) => { // Handle member join event @@ -100,11 +127,12 @@ fn configure(settings: &mut TikTokLiveSettings) { // Function to configure the TikTok live settings with cookies for authentication fn configure_with_cookies(settings: &mut TikTokLiveSettings) { settings.http_data.time_out = Duration::from_secs(12); // Set HTTP timeout to 12 seconds - let contents = "YOUR_COOKIES"; // Placeholder for cookies + let contents = ""; // Placeholder for cookies settings .http_data .headers - .insert("Cookie".to_string(), contents.to_string()); // Insert cookies into HTTP headers + .insert("Cookie".to_string(), contents.to_string()); + // Insert cookies into HTTP headers } // Function to create a TikTok live client for the given username