This commit is contained in:
JW
2024-01-14 22:55:05 +01:00
parent 72092bb56b
commit 913d473442
25 changed files with 1227 additions and 0 deletions

View File

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>TikTokLiveJava</artifactId>
<groupId>io.github.jwdeveloper.tiktok</groupId>
<version>1.0.14-Release</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>extension-recorder</artifactId>
<dependencies>
<dependency>
<groupId>io.github.jwdeveloper.tiktok</groupId>
<artifactId>Client</artifactId>
<version>1.0.14-Release</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.bramp.ffmpeg</groupId>
<artifactId>ffmpeg</artifactId>
<version>0.8.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.github.jwdeveloper.tiktok</groupId>
<artifactId>API</artifactId>
<version>1.0.17-Release</version>
<scope>compile</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>16</maven.compiler.source>
<maven.compiler.target>16</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2023-2023 jwdeveloper jacekwoln@gmail.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.github.jwdeveloper.tiktok.extension.recorder;
import io.github.jwdeveloper.tiktok.extension.recorder.api.LiveRecorder;
import io.github.jwdeveloper.tiktok.extension.recorder.impl.RecorderListener;
import io.github.jwdeveloper.tiktok.extension.recorder.impl.data.RecorderSettings;
import java.util.function.Consumer;
public class TikTokLiveRecorder
{
public static LiveRecorder use(Consumer<RecorderSettings> consumer)
{
return new RecorderListener(consumer);
}
public static LiveRecorder use()
{
return use(x ->{});
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (c) 2023-2023 jwdeveloper jacekwoln@gmail.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.github.jwdeveloper.tiktok.extension.recorder.api;
import io.github.jwdeveloper.tiktok.listener.TikTokEventListener;
public interface LiveRecorder extends TikTokEventListener {
void pause();
void unpause();
}

View File

@@ -0,0 +1,251 @@
/*
* Copyright (c) 2023-2023 jwdeveloper jacekwoln@gmail.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.github.jwdeveloper.tiktok.extension.recorder.impl;
import com.google.gson.JsonParser;
import io.github.jwdeveloper.tiktok.annotations.TikTokEventObserver;
import io.github.jwdeveloper.tiktok.data.events.TikTokLiveEndedEvent;
import io.github.jwdeveloper.tiktok.data.settings.LiveClientSettings;
import io.github.jwdeveloper.tiktok.extension.recorder.api.LiveRecorder;
import io.github.jwdeveloper.tiktok.data.events.TikTokConnectedEvent;
import io.github.jwdeveloper.tiktok.data.events.TikTokDisconnectedEvent;
import io.github.jwdeveloper.tiktok.exceptions.TikTokLiveException;
import io.github.jwdeveloper.tiktok.extension.recorder.impl.data.DownloadData;
import io.github.jwdeveloper.tiktok.extension.recorder.impl.data.RecorderSettings;
import io.github.jwdeveloper.tiktok.data.events.http.TikTokRoomDataResponseEvent;
import io.github.jwdeveloper.tiktok.extension.recorder.impl.enums.LiveQuality;
import io.github.jwdeveloper.tiktok.extension.recorder.impl.event.TikTokLiveRecorderStartedEvent;
import io.github.jwdeveloper.tiktok.http.HttpClientFactory;
import io.github.jwdeveloper.tiktok.live.LiveClient;
import net.bramp.ffmpeg.FFmpeg;
import net.bramp.ffmpeg.FFmpegExecutor;
import net.bramp.ffmpeg.RunProcessFunction;
import net.bramp.ffmpeg.builder.FFmpegBuilder;
import java.io.*;
import java.net.URL;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import static java.nio.charset.StandardCharsets.UTF_8;
public class RecorderListener implements LiveRecorder {
private final Consumer<RecorderSettings> consumer;
private RecorderSettings settings;
private DownloadData downloadData;
private Thread liveDownloadThread;
public RecorderListener(Consumer<RecorderSettings> consumer) {
this.consumer = consumer;
}
@Override
public void pause() {
}
@Override
public void unpause() {
}
@TikTokEventObserver
private void onResponse(LiveClient liveClient, TikTokRoomDataResponseEvent event) {
settings = RecorderSettings.DEFAULT();
consumer.accept(settings);
var json = event.getLiveData().getJson();
liveClient.getLogger().info("Searching for live download url");
if (settings.getPrepareDownloadData() != null) {
downloadData = settings.getPrepareDownloadData().apply(json);
} else {
downloadData = mapToDownloadData(json);
}
if (downloadData.getDownloadLiveUrl().isEmpty()) {
throw new TikTokLiveException("Unable to find download live url!");
}
liveClient.getLogger().info("Live download url found!");
}
@TikTokEventObserver
private void onConnected(LiveClient liveClient, TikTokConnectedEvent event) {
/* liveDownloadThread = new Thread(() ->
{
try {
var ffmpeg = new FFmpeg(settings.getFfmpegPath(), new RunProcessFunction() {
@Override
public Process run(final List<String> args) throws IOException {
var process = super.run(args);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
terminateFfmpeg(process);
}, "FFmpeg process destroyer"));
return process;
}
});
var builder = new FFmpegBuilder()
.setInput(downloadData.getFullUrl())
.addOutput(settings.getOutputPath() + File.separator + settings.getOutputFileName()) // Set the output file path
.setFormat("mp4")
.done();
var executor = new FFmpegExecutor(ffmpeg);
var ffmpegProcess = executor.createJob(builder, (progress)-> {
liveClient.getLogger().info("Downloading stream: " +progress.total_size);
});
ffmpegProcess.run();
liveClient.publishEvent(new TikTokLiveRecorderStartedEvent(downloadData));
} catch (Exception e) {
throw new TikTokLiveException("Unable to run ffmpeg drivers",e);
}
});
*/
var factory = new HttpClientFactory(LiveClientSettings.createDefault());
var builder = factory.client(downloadData.getFullUrl());
/*
var path = settings.getOutputPath() + File.separator + settings.getOutputFileName();
var file = new File(path);
file.getParentFile().mkdirs();
*/
liveDownloadThread = new Thread(() ->
{
var bufferSize = 1024;
try (var in = new BufferedInputStream(new URL(downloadData.getFullUrl()).openStream(), bufferSize)) {
var path = settings.getOutputPath() + File.separator + settings.getOutputFileName();
var file = new File(path);
file.getParentFile().mkdirs();
var fileOutputStream = new FileOutputStream(file);
byte dataBuffer[] = new byte[bufferSize];
int bytesRead;
while ((bytesRead = in.read(dataBuffer, 0, bufferSize)) != -1) {
fileOutputStream.write(dataBuffer, 0, bytesRead);
}
} catch (IOException e) {
e.printStackTrace();
}
});
liveDownloadThread.start();
}
private static void downloadUsingStream(String urlStr, String file) throws IOException {
URL url = new URL(urlStr);
BufferedInputStream bis = new BufferedInputStream(url.openStream());
FileOutputStream fis = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int count = 0;
while ((count = bis.read(buffer, 0, 1024)) != -1) {
fis.write(buffer, 0, count);
}
fis.close();
bis.close();
}
@TikTokEventObserver
private void onDisconnected(LiveClient liveClient, TikTokDisconnectedEvent event) {
liveDownloadThread.interrupt();
}
@TikTokEventObserver
private void onDisconnected(LiveClient liveClient, TikTokLiveEndedEvent event) {
liveDownloadThread.interrupt();
}
private int terminateFfmpeg(final Process process) {
if (!process.isAlive()) {
/*
* ffmpeg -version, do nothing
*/
return process.exitValue();
}
/*
* ffmpeg -f x11grab
*/
System.out.println("About to destroy the child process...");
try (final OutputStreamWriter out = new OutputStreamWriter(process.getOutputStream(), UTF_8)) {
out.write('q');
} catch (final IOException ioe) {
ioe.printStackTrace();
}
try {
if (!process.waitFor(5L, TimeUnit.SECONDS)) {
process.destroy();
process.waitFor();
}
return process.exitValue();
} catch (final InterruptedException ie) {
System.out.println("Interrupted");
ie.printStackTrace();
Thread.currentThread().interrupt();
return -1;
}
}
private DownloadData mapToDownloadData(String json) {
var parsedJson = JsonParser.parseString(json);
var jsonObject = parsedJson.getAsJsonObject();
var streamDataJson = jsonObject.getAsJsonObject("data")
.getAsJsonObject("stream_url")
.getAsJsonObject("live_core_sdk_data")
.getAsJsonObject("pull_data")
.get("stream_data")
.getAsString();
var streamDataJsonObject = JsonParser.parseString(streamDataJson).getAsJsonObject();
var urlLink = streamDataJsonObject.getAsJsonObject("data")
.getAsJsonObject(LiveQuality.origin.name())
.getAsJsonObject("main")
.get("flv")
.getAsString();
var sessionId = streamDataJsonObject.getAsJsonObject("common")
.get("session_id")
.getAsString();
//main
//https://pull-f5-tt03.fcdn.eu.tiktokcdn.com/stage/stream-3284937501738533765.flv?session_id=136-20240109000954BF818F1B3A8E5E39E238&_webnoredir=1
//Working
//https://pull-f5-tt03.fcdn.eu.tiktokcdn.com/game/stream-3284937501738533765_sd5.flv?_session_id=136-20240109001052D91FDBC00143211020C8.1704759052997&_webnoredir=1
//https://pull-f5-tt02.fcdn.eu.tiktokcdn.com/stage/stream-3861399216374940610_uhd5.flv?_session_id=136-20240109000223D0BAA1A83974490EE630.1704758544391&_webnoredir=1
return new DownloadData(urlLink, sessionId);
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (c) 2023-2023 jwdeveloper jacekwoln@gmail.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.github.jwdeveloper.tiktok.extension.recorder.impl.data;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class DownloadData {
private String downloadLiveUrl;
private String sessionId;
public String getFullUrl() {
return downloadLiveUrl + "?_webnoredir=1&session_id=" + sessionId;
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright (c) 2023-2023 jwdeveloper jacekwoln@gmail.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.github.jwdeveloper.tiktok.extension.recorder.impl.data;
import io.github.jwdeveloper.tiktok.extension.recorder.impl.enums.LiveQuality;
import io.github.jwdeveloper.tiktok.extension.recorder.impl.enums.LiveFormat;
import lombok.Getter;
import lombok.Setter;
import java.util.function.Function;
/**
* self,
* path: str,
* duration: Optional[int] = None,
* quality: Optional[VideoQuality] = None,
* verbose: bool = True,
* loglevel: str = "error",
* global_options: Set[str] = set(),
* inputs: Dict[str, str] = dict(),
* outputs: Dict[str, str] = dict()
* :param loglevel: Set the FFmpeg log level
* :param outputs: Pass custom params to FFmpeg outputs
* :param inputs: Pass custom params to FFmpeg inputs
* :param global_options: Pass custom params to FFmpeg global options
* :param path: The path to download the livestream video to
* :param duration: If duration is None or less than 1, download will go forever
* :param quality: If quality is None, download quality will auto
* :param verbose: Whether to log info about the download in console
*/
@Getter
@Setter
public class RecorderSettings {
private String ffmpegPath;
private String quality;
private String format;
private String outputPath;
private String outputFileName;
private Function<String,DownloadData> prepareDownloadData;
private boolean startOnConnected;
public static RecorderSettings DEFAULT() {
return new RecorderSettings();
}
public void setQuality(String format) {
this.format = format;
}
public void setQuality(LiveQuality quality) {
this.quality = quality.name();
}
public void setFormat(String format) {
this.format = format;
}
public void setFormat(LiveFormat format) {
this.format = format.name();
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright (c) 2023-2023 jwdeveloper jacekwoln@gmail.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.github.jwdeveloper.tiktok.extension.recorder.impl.enums;
public enum LiveFormat
{
MP4
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright (c) 2023-2023 jwdeveloper jacekwoln@gmail.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.github.jwdeveloper.tiktok.extension.recorder.impl.enums;
public enum LiveQuality {
origin, hd_60, ao, hd, sd, ld,uhd_60
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright (c) 2023-2023 jwdeveloper jacekwoln@gmail.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.github.jwdeveloper.tiktok.extension.recorder.impl.event;
import io.github.jwdeveloper.tiktok.data.events.common.TikTokEvent;
import io.github.jwdeveloper.tiktok.extension.recorder.impl.data.DownloadData;
import lombok.AllArgsConstructor;
import lombok.Data;
@AllArgsConstructor
@Data
public class TikTokLiveRecorderStartedEvent extends TikTokEvent
{
DownloadData downloadData;
}