Skip to content

Commit 8190fe1

Browse files
committed
add support TikTok
1 parent e88c436 commit 8190fe1

File tree

7 files changed

+349
-0
lines changed

7 files changed

+349
-0
lines changed

public/images/logos/tiktok.png

44.7 KB
Loading

public/request.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ <h1>Share media from one of the selected services</h1>
1616

1717
<div class=services>
1818
<span class="media-service logo-youtube" data-href="https://www.youtube.com/" data-name="YouTube" data-service="yt" onclick=selectService(this) onmouseover=hoverService()>&nbsp;</span>
19+
<span class="media-service logo-tiktok" data-href="https://www.tiktok.com/" data-name="TikTok" data-service="tt" onclick=selectService(this) onmouseover=hoverService()>&nbsp;</span>
1920
<span class="media-service subtext logo-twitch" data-href="https://www.twitch.tv/" data-name="Twitch" data-service="twl" onclick=selectService(this) onmouseover=hoverService()>&nbsp;</span>
2021
<span class="media-service subtext logo-soundcloud" data-href="https://soundcloud.com/discover" data-name="SoundCloud" data-service="sc" onclick=selectService(this) onmouseover=hoverService()>&nbsp;</span>
2122
<span class="media-service subtext logo-dailymotion" data-href="https://www.dailymotion.com/" data-name="Dailymotion" data-service="dm" onclick=selectService(this) onmouseover=hoverService()>&nbsp;</span>

public/styles/request.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ h1 {
288288
}
289289

290290
.logo-reddit { background-image: url(../images/logos/reddit.png) }
291+
.logo-tiktok { background-image: url(../images/logos/tiktok.png) }
291292
.logo-archive { background-image: url(../images/logos/archive.png); }
292293
.logo-dailymotion { background-image: url(../images/logos/dailymotion.png); }
293294
.logo-twitch { background-image: url(../images/logos/twitch.png) }
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
include "shared.lua"
2+
3+
DEFINE_BASECLASS( "mp_service_browser" )
4+
5+
local EMBED_PARAM = "?controls=0&fullscreen_button=0&play_button=0&volume_control=0&timestamp=0&loop=0&description=0&music_info=0&rel=0&autoplay=1"
6+
local EMBED_URL = "https://www.tiktok.com/embed/v3/%s" .. EMBED_PARAM
7+
8+
local JS_Interface = [[
9+
(async function() {
10+
let cookieClicked = false;
11+
let playerReady = false;
12+
const startTime = Date.now();
13+
14+
const observePlayer = () => {
15+
return new Promise((resolve) => {
16+
const observer = new MutationObserver(async (mutations, obs) => {
17+
const player = document.querySelector("video");
18+
19+
if (player && !playerReady) {
20+
// Handle cookie banner first
21+
const banner = document.querySelector("tiktok-cookie-banner");
22+
if (banner && !cookieClicked) {
23+
const buttons = banner.shadowRoot?.querySelectorAll(".tiktok-cookie-banner .button-wrapper button");
24+
if (buttons?.[0]) {
25+
buttons[0].click();
26+
cookieClicked = true;
27+
return;
28+
}
29+
cookieClicked = true;
30+
}
31+
32+
// Wait for video to be ready
33+
if (player.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
34+
playerReady = true;
35+
obs.disconnect();
36+
37+
// Setup video controls
38+
player.setAttribute('controls', '');
39+
40+
// Simulate click to unmute (crucial for browser policies)
41+
player.click();
42+
43+
// Ensure unmuted state
44+
player.muted = false;
45+
46+
window.MediaPlayer = player
47+
resolve(player);
48+
}
49+
} else if (Date.now() - startTime > 10000 && !playerReady) {
50+
obs.disconnect();
51+
console.log("Video player not found or not ready");
52+
resolve(null);
53+
}
54+
});
55+
56+
observer.observe(document.body, {
57+
childList: true,
58+
subtree: true,
59+
attributes: true,
60+
attributeFilter: ['readyState', 'muted', 'volume']
61+
});
62+
});
63+
};
64+
65+
await observePlayer();
66+
})();
67+
]]
68+
69+
function SERVICE:OnBrowserReady( browser )
70+
71+
-- Resume paused player
72+
if self._MediaPaused then
73+
self.Browser:RunJavascript( [[
74+
if(window.MediaPlayer) {
75+
MediaPlayer.play()
76+
}
77+
]] )
78+
79+
self._MediaPaused = nil
80+
return
81+
end
82+
83+
BaseClass.OnBrowserReady( self, browser )
84+
85+
local mediaID = self:GetMediaID()
86+
local url = EMBED_URL:format(mediaID)
87+
88+
browser:OpenURL(url)
89+
browser.OnDocumentReady = function(pnl)
90+
browser:QueueJavascript( JS_Interface )
91+
end
92+
93+
end
94+
95+
do -- Player Controls
96+
local JS_Pause = "if(window.MediaPlayer) MediaPlayer.pause();"
97+
local JS_Volume = "if(window.MediaPlayer) MediaPlayer.volume = %s;"
98+
local JS_Seek = [[
99+
if (window.MediaPlayer) {
100+
var seekTime = %s;
101+
var curTime = window.MediaPlayer.currentTime;
102+
103+
var diffTime = Math.abs(curTime - seekTime);
104+
if (diffTime > 5) {
105+
window.MediaPlayer.currentTime = seekTime
106+
}
107+
}
108+
]]
109+
110+
function SERVICE:Pause()
111+
BaseClass.Pause( self )
112+
113+
if IsValid(self.Browser) then
114+
self.Browser:RunJavascript(JS_Pause)
115+
self._MediaPaused = true
116+
end
117+
118+
end
119+
120+
function SERVICE:SetVolume( volume )
121+
local js = JS_Volume:format( MediaPlayer.Volume() )
122+
self.Browser:RunJavascript(js)
123+
end
124+
125+
function SERVICE:Sync()
126+
127+
local seekTime = self:CurrentTime()
128+
if IsValid(self.Browser) and self:IsTimed() and seekTime > 0 then
129+
self.Browser:RunJavascript(JS_Seek:format(seekTime))
130+
end
131+
end
132+
end
133+
134+
function SERVICE:IsMouseInputEnabled()
135+
return IsValid( self.Browser )
136+
end
137+
138+
do -- Metadata Prefech
139+
local METADATA_JS = [[
140+
setTimeout(function(){
141+
(async () => {
142+
var contentID = "{@contentID}"
143+
var videosrc = document.querySelector(`[href$=\"${contentID}\"]`)
144+
145+
if (!videosrc) {
146+
videosrc = { href: `https://www.tiktok.com/@unknown/video/${contentID}` };
147+
}
148+
149+
try {
150+
const response = await fetch(`https://www.tiktok.com/oembed?url=${videosrc.href}`)
151+
const json = await response.json()
152+
153+
// Check if video is embeddable
154+
if (json.error || !json.html) {
155+
console.log("ERROR:Video is not embeddable or private")
156+
return;
157+
}
158+
159+
var player = document.getElementsByTagName("VIDEO")[0]
160+
if (!!player) {
161+
// Wait for metadata to load with timeout
162+
var attempts = 0;
163+
var maxAttempts = 10; // 5 seconds
164+
165+
var checkDuration = setInterval(function() {
166+
attempts++;
167+
168+
if (player.duration && player.duration > 0 && !isNaN(player.duration)) {
169+
clearInterval(checkDuration);
170+
171+
var title = json.title.length == 0 && `@${json.author_name} (${contentID})` || json.title.substr(0, 75) + " ..."
172+
var metadata = {
173+
duration: Math.round(player.duration),
174+
title: title
175+
}
176+
177+
console.log("METADATA:" + JSON.stringify(metadata))
178+
} else if (attempts >= maxAttempts) {
179+
clearInterval(checkDuration);
180+
console.log("ERROR:Video duration cannot be detected after timeout")
181+
}
182+
}, 500);
183+
} else {
184+
console.log("ERROR:Video player not found - may not be embeddable")
185+
}
186+
} catch (error) {
187+
console.log("ERROR:Failed to fetch video metadata - " + error.message)
188+
}
189+
})()
190+
}, 500)
191+
]]
192+
193+
function SERVICE:PreRequest( callback )
194+
195+
local mediaID = self:GetMediaID()
196+
197+
local panel = vgui.Create("DHTML")
198+
panel:SetSize(500,500)
199+
panel:SetAlpha(0)
200+
panel:SetMouseInputEnabled(false)
201+
202+
svc = self
203+
function panel:ConsoleMessage(msg)
204+
205+
if msg:StartWith("ERROR:") then
206+
local errmsg = string.sub(msg, 7)
207+
208+
callback(errmsg)
209+
panel:Remove()
210+
return
211+
end
212+
213+
if msg:StartWith("METADATA:") then
214+
local metadata = util.JSONToTable(string.sub(msg, 10))
215+
216+
svc._metaTitle = metadata.title
217+
svc._metaDuration = metadata.duration
218+
callback()
219+
panel:Remove()
220+
end
221+
end
222+
223+
local js = METADATA_JS
224+
js = js:Replace("{@contentID}", mediaID)
225+
226+
function panel:OnDocumentReady(url)
227+
if IsValid(panel) then
228+
panel:QueueJavascript(js)
229+
end
230+
end
231+
232+
panel:OpenURL(EMBED_URL:format(mediaID))
233+
234+
timer.Simple(10, function()
235+
if IsValid(panel) then
236+
panel:Remove()
237+
end
238+
end )
239+
end
240+
241+
function SERVICE:NetWriteRequest()
242+
if self._metaTitle then net.WriteString( self._metaTitle ) end
243+
if self._metaDuration then net.WriteUInt( self._metaDuration, 16 ) end
244+
end
245+
end
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
AddCSLuaFile "shared.lua"
2+
include "shared.lua"
3+
4+
function SERVICE:GetMetadata( callback )
5+
if self._metadata then
6+
callback( self._metadata )
7+
return
8+
end
9+
10+
local cache = MediaPlayer.Metadata:Query(self)
11+
12+
if MediaPlayer.DEBUG then
13+
print("MediaPlayer.GetMetadata Cache results:")
14+
PrintTable(cache or {})
15+
end
16+
17+
if cache then
18+
19+
local metadata = {}
20+
metadata.title = cache.title
21+
metadata.duration = tonumber(cache.duration)
22+
23+
self:SetMetadata(metadata)
24+
MediaPlayer.Metadata:Save(self)
25+
26+
callback(self._metadata)
27+
28+
else
29+
30+
-- local videoId = self:GetMediaID()
31+
local metadata = {}
32+
33+
-- Title & Duration is taken from Client via PreRequest
34+
metadata.title = self._metaTitle
35+
metadata.duration = self._metaDuration
36+
37+
self:SetMetadata(metadata, true)
38+
MediaPlayer.Metadata:Save(self)
39+
40+
callback(self._metadata)
41+
end
42+
end
43+
44+
function SERVICE:NetReadRequest()
45+
46+
if not self.PrefetchMetadata then return end
47+
48+
self._metaTitle = net.ReadString()
49+
self._metaDuration = net.ReadUInt( 16 )
50+
51+
end
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
DEFINE_BASECLASS( "mp_service_base" )
2+
3+
SERVICE.Name = "TikTok"
4+
SERVICE.Id = "tt"
5+
SERVICE.Base = "browser"
6+
7+
SERVICE.PrefetchMetadata = true
8+
9+
function SERVICE:New( url )
10+
local obj = BaseClass.New(self, url)
11+
obj._data = obj:GetMediaID()
12+
return obj
13+
end
14+
15+
function SERVICE:Match( url )
16+
return url:match("tiktok.com")
17+
end
18+
19+
function SERVICE:IsTimed()
20+
if self._istimed == nil then
21+
self._istimed = self:Duration() > 0
22+
end
23+
24+
return self._istimed
25+
end
26+
27+
function SERVICE:GetMediaID()
28+
29+
local videoId
30+
31+
if self.videoId then
32+
33+
videoId = self.videoId
34+
35+
elseif self.urlinfo then
36+
37+
local url = self.urlinfo
38+
39+
-- https://drive.google.com/file/d/(fileId)
40+
if url.path and url.path:match("/@[%a%w%d%_%.]+/video/(%d+)$") then
41+
videoId = url.path:match("/@[%a%w%d%_%.]+/video/(%d+)$")
42+
end
43+
44+
self.videoId = videoId
45+
46+
end
47+
48+
return videoId
49+
50+
end

workshop/lua/mediaplayer/sh_services.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ do
107107
"dailymotion",
108108
"archive",
109109
"gdrive",
110+
"tiktok",
110111

111112
-- HTML Resources
112113
"resource", -- base

0 commit comments

Comments
 (0)