Modify Websocket GET BASE64 mechanism to get concise FPS between HQ and LQ.

Support multiple quality between users.
This commit is contained in:
2024-11-11 11:34:29 +07:00
parent 2450f9f42a
commit 1fe4716bab
15 changed files with 131 additions and 10840 deletions

View File

@@ -1,9 +1,7 @@
package Camera;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import Other.SomeCodes;
@@ -11,24 +9,19 @@ import lombok.Getter;
import lombok.Setter;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.FrameGrabber;
import org.bytedeco.opencv.opencv_core.Mat;
public class GrabbingTask implements Runnable {
@Setter private Consumer<String> onMessageUpdate;
@Setter private Consumer<Mat> onMatUpdate;
@Setter private Consumer<Frame> onFrameUpdate;
@Setter private Consumer<String> onBase64Update;
@Setter private Consumer<String> onStreamingStatusUpdate;
@Setter private Consumer<Frame> onHQFrameUpdate;
@Setter private Consumer<Frame> onLQFrameUpdate;
@Setter private Consumer<String> onHQBase64Update;
@Setter private Consumer<String> onLQBase64Update;
private final AtomicBoolean isGrabbing;
private final FrameGrabber grabber;
@Getter private final int lowquality_width = 640;
@Getter private final int lowquality_height = 360;
@Getter @Setter private boolean HQ = false;
@Getter private int streaming_width = 0;
@Getter private int streaming_height = 0;
@Getter private int streaming_fps = 0;
AtomicBoolean streamingstatuschanged = new AtomicBoolean(false);
private void updateMessage(String message) {
if (onMessageUpdate != null) {
@@ -36,87 +29,66 @@ public class GrabbingTask implements Runnable {
}
}
private void updateMat(Mat value) {
if (onMatUpdate != null) {
onMatUpdate.accept(value);
private void updateHQBase64(String base64) {
if (onHQBase64Update != null) {
onHQBase64Update.accept(base64);
}
}
private void updateBase64(String base64) {
if (onBase64Update != null) {
onBase64Update.accept(base64);
private void updateLQBase64(String base64) {
if (onLQBase64Update != null) {
onLQBase64Update.accept(base64);
}
}
private void updateFrame(Frame frame) {
if (onFrameUpdate != null) {
onFrameUpdate.accept(frame);
private void updateHQFrame(Frame frame) {
if (onHQFrameUpdate != null) {
onHQFrameUpdate.accept(frame);
}
}
private void updateStreamingStatus(String status) {
if (onStreamingStatusUpdate != null) {
onStreamingStatusUpdate.accept(status);
private void updateLQFrame(Frame frame) {
if (onLQFrameUpdate != null) {
onLQFrameUpdate.accept(frame);
}
}
public GrabbingTask(AtomicBoolean isGrabbing, FrameGrabber grabber) {
this.isGrabbing = isGrabbing;
this.grabber = grabber;
}
public String GetStreamingStatus(){
return "Streaming at " + streaming_width + "x" + streaming_height + " " + streaming_fps + "fps";
}
@Override
public void run() {
isGrabbing.set(true);
AtomicInteger framecount = new AtomicInteger(0);
TimerTask task = new TimerTask() {
@Override
public void run() {
if (streaming_fps != framecount.get()) {
streaming_fps = framecount.get();
streamingstatuschanged.set(true);
}
framecount.set(0);
if (streamingstatuschanged.get()) {
updateStreamingStatus(GetStreamingStatus());
streamingstatuschanged.set(false);
}
}
};
Timer timer = new Timer();
timer.scheduleAtFixedRate(task, 1000, 1000);
while (isGrabbing.get()) {
try {
//Thread.sleep(100); // 10 fps
Frame fr =grabber.grab();
if (fr!=null){
if (!HQ) fr = SomeCodes.ResizeFrame(fr, lowquality_width, lowquality_height);
updateFrame(fr);
updateBase64(SomeCodes.BufferedImageToBase64(SomeCodes.FrameToBufferedImage(fr)));
Mat mat = SomeCodes.matConverter.convert(fr);
updateMat(mat);
if (streaming_width != fr.imageWidth) {
streaming_width = fr.imageWidth;
streamingstatuschanged.set(true);
}
if (streaming_height != fr.imageHeight) {
streaming_height = fr.imageHeight;
streamingstatuschanged.set(true);
}
framecount.incrementAndGet();
updateHQFrame(fr);
updateHQBase64(SomeCodes.FrameToBase64(fr));
Frame resized = SomeCodes.ResizeFrame(fr, lowquality_width, lowquality_height);
updateLQFrame(resized);
updateLQBase64(SomeCodes.FrameToBase64(resized));
} else updateMessage("Grabber returned null frame");
} catch (Exception e) {
updateMessage("Error grabbing frame: " + e.getMessage());
}
}
timer.cancel();
}
}

View File

@@ -1,11 +0,0 @@
package Camera;
import org.bytedeco.javacv.Frame;
import org.bytedeco.opencv.opencv_core.Mat;
public interface RtspEvent {
void onMatReceived(Mat mat);
void onFrameReceived(Frame frame);
void onBase64Received(String base64);
void onStreamingStatusReceived(String status);
}

View File

@@ -3,26 +3,22 @@ import lombok.Getter;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.opencv.opencv_core.Mat;
import org.tinylog.Logger;
import java.util.concurrent.atomic.AtomicBoolean;
@SuppressWarnings("unused")
public class RtspGrabber {
private final String rtspUrl;
private FFmpegFrameGrabber grabber;
private final AtomicBoolean isGrabbing = new AtomicBoolean(false);
private @Getter Frame lastFrame = null;
private @Getter String lastBase64 = null;
private @Getter Mat lastMat = null;
private GrabbingTask grabbingTask = null;
public RtspGrabber(String ip, int port, String username, String password, String path) {
rtspUrl = "rtsp://" + username + ":" + password + "@" + ip + ":" + port + path;
Logger.info("RtspGrabber created with url: " + rtspUrl);
}
private @Getter Frame lastHQFrame = null;
private @Getter Frame lastLQFrame = null;
private @Getter String lastHQBase64 = null;
private @Getter String lastLQBase64 = null;
private @Getter int HQWidth = 0;
private @Getter int HQHeight = 0;
private @Getter int LQWidth = 0;
private @Getter int LQHeight = 0;
public RtspGrabber(String ip, String path) {
this.rtspUrl = "rtsp://" + ip + path;
@@ -32,16 +28,15 @@ public class RtspGrabber {
/**
* Start grabbing frames from rtsp
* @param useTcp Use tcp instead of udp
* @param event Event to be called when frame is received
*/
public void Start(boolean useTcp, final int width, final int height, RtspEvent event){
public void Start(boolean useTcp, final int width, final int height){
try{
grabber = FFmpegFrameGrabber.createDefault(rtspUrl);
if (useTcp) grabber.setOption("rtsp_transport", "tcp");
//grabber.setImageWidth(width);
//grabber.setImageHeight(height);
grabber.setImageWidth(width);
grabber.setImageHeight(height);
grabber.setPixelFormat(avutil.AV_PIX_FMT_BGR24);
grabber.start();
avutil.av_log_set_level(avutil.AV_LOG_ERROR);
@@ -51,29 +46,28 @@ public class RtspGrabber {
Logger.info("Grabber started");
GrabbingTask tt = new GrabbingTask(isGrabbing, grabber);
tt.setOnMessageUpdate(Logger::info);
tt.setOnMatUpdate(value -> {
// Kalau butuh Mat untuk diproses
lastMat = value;
if (event!=null) event.onMatReceived(value);
});
tt.setOnFrameUpdate(value -> {
// Kalau butuh Frame untuk ditampilkan
lastFrame = value;
tt.setOnHQFrameUpdate(value -> {
if (value!=null){
if (value.imageWidth>0 && value.imageHeight>0){
lastHQFrame = value;
HQWidth = value.imageWidth;
HQHeight = value.imageHeight;
}
if (event!=null) event.onFrameReceived(value);
}
});
tt.setOnBase64Update(value -> {
// Kalau butuh Base64 untuk dikirim ke Websocket
lastBase64 = value;
if (event!=null) event.onBase64Received(value);
tt.setOnLQFrameUpdate(value -> {
if (value!=null){
if (value.imageWidth>0 && value.imageHeight>0){
lastLQFrame = value;
LQWidth = value.imageWidth;
LQHeight = value.imageHeight;
}
}
});
tt.setOnStreamingStatusUpdate(value -> {
// Kalau butuh status streaming
if (event!=null) event.onStreamingStatusReceived(value);
});
tt.setOnHQBase64Update(value -> lastHQBase64 = value);
tt.setOnLQBase64Update(value -> lastLQBase64 = value);
new Thread(tt).start();
grabbingTask = tt;
} catch (Exception e){
Logger.error("Error starting grabber: " + e.getMessage());
@@ -95,26 +89,6 @@ public class RtspGrabber {
}
}
/**
* Check if grabber is grabbing
* @return True if grabbing
*/
public boolean IsGrabbing(){
return isGrabbing.get();
}
public void ChangeVideoQuality(boolean HQ){
if (IsGrabbing()){
if (grabbingTask!=null){
grabbingTask.setHQ(HQ);
}
}
}
public String GetStreamingStatus(){
if (grabbingTask!=null){
return grabbingTask.GetStreamingStatus();
}
return "No Status";
}
}

View File

@@ -2,7 +2,6 @@ package Camera;
import org.tinylog.Logger;
import java.awt.*;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

View File

@@ -1,5 +1,6 @@
package Other;
import org.bytedeco.javacpp.Loader;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;
import org.bytedeco.javacv.OpenCVFrameConverter;
@@ -8,10 +9,12 @@ import org.bytedeco.opencv.global.opencv_imgproc;
import org.bytedeco.opencv.opencv_core.Mat;
import org.bytedeco.opencv.opencv_core.Size;
import org.bytedeco.opencv.opencv_core.UMat;
import org.bytedeco.opencv.opencv_java;
import org.jetbrains.annotations.NotNull;
import org.opencv.core.MatOfByte;
import org.opencv.imgcodecs.Imgcodecs;
import org.tinylog.Logger;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.Inet4Address;
import java.net.Inet6Address;
@@ -19,19 +22,26 @@ import java.net.InetAddress;
import java.nio.file.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.Properties;
@SuppressWarnings("unused")
public class SomeCodes {
static{
Loader.load(opencv_java.class);
}
public final static String currentDirectory = System.getProperty("user.dir");
public final static Path audioPath = Path.of(currentDirectory, "audiofiles");
private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static final OpenCVFrameConverter.ToMat matConverter = new OpenCVFrameConverter.ToMat();
public static final OpenCVFrameConverter.ToOrgOpenCvCoreMat CoreMatConverter = new OpenCVFrameConverter.ToOrgOpenCvCoreMat();
public static final Java2DFrameConverter frameConverter = new Java2DFrameConverter();
public static final Path logsPath = Path.of(currentDirectory, "logs");
public static final boolean haveOpenCL = opencv_core.haveOpenCL();
public static boolean useOpenCL;
private static final Base64.Encoder base64encoder = java.util.Base64.getEncoder();
public static String[] GetAudioFiles(){
try{
@@ -168,31 +178,28 @@ public class SomeCodes {
return "";
}
public static BufferedImage FrameToBufferedImage(Frame frame){
return frameConverter.getBufferedImage(frame);
}
public static BufferedImage MatToBufferedImage(Mat mat){
return frameConverter.getBufferedImage(matConverter.convert(mat));
}
public static String BufferedImageToBase64(BufferedImage image){
if (image!=null){
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try{
javax.imageio.ImageIO.write(image, "jpg", baos);
baos.flush();
byte[] imageInByte = baos.toByteArray();
baos.close();
return java.util.Base64.getEncoder().encodeToString(imageInByte);
} catch (Exception e){
Logger.error("Error converting BufferedImage to Base64: "+e.getMessage());
// Function ini pakai opencv, bukan javacv, jadi perlu Loader.load(opencv_java.class) di awal
// lebih optimal untuk konversi frame ke base64
public static String FrameToBase64(Frame frame){
if (frame!=null){
org.opencv.core.Mat converted = CoreMatConverter.convert(frame);
if (converted!=null){
if (!converted.empty()){
MatOfByte mob = new MatOfByte();
Imgcodecs.imencode(".jpg", converted, mob);
byte[] jpgdata = mob.toArray();
mob.release();
converted.release();
return base64encoder.encodeToString(jpgdata);
}
}
}
return "";
}
public static @NotNull Properties LoadProperties(String filename){
try{
InputStream is = new FileInputStream(filename);

View File

@@ -5,7 +5,6 @@ import io.javalin.Javalin;
import io.javalin.http.UploadedFile;
import io.javalin.util.JavalinException;
import io.javalin.websocket.*;
import lombok.extern.java.Log;
import org.tinylog.Logger;
import java.nio.file.Files;

View File

@@ -3,8 +3,16 @@ package Web;
public class WebsocketReply {
public String reply;
public String data;
public String additional;
public WebsocketReply(String reply, String data){
this.reply = reply;
this.data = data;
this.additional = "";
}
public WebsocketReply(String reply, String data, String additional){
this.reply = reply;
this.data = data;
this.additional = additional;
}
}

View File

@@ -4,7 +4,6 @@ import Audio.AudioFileProperties;
import Audio.AudioPlayer;
import Audio.PlaybackEvent;
import Camera.PanTiltController;
import Camera.RtspEvent;
import Camera.RtspGrabber;
import Camera.VapixProtocol;
import Other.SomeCodes;
@@ -12,9 +11,7 @@ import Web.WebServer;
import Web.WebsocketCommand;
import Web.WebsocketEvent;
import Web.WebsocketReply;
import org.bytedeco.javacv.Frame;
import org.bytedeco.opencv.global.opencv_core;
import org.bytedeco.opencv.opencv_core.Mat;
import org.tinylog.Logger;
import java.io.File;
@@ -119,30 +116,8 @@ public class Main {
if (ValidString(rtsppath)){
rtspGrabber = new RtspGrabber(targetip, rtsppath);
RtspEvent re = new RtspEvent() {
@Override
public void onMatReceived(Mat mat) {
//TODO : kalau butuh Mat, ambil disini
}
@Override
public void onFrameReceived(Frame frame) {
//TODO : kalau butuh Frame, ambil disini
}
@Override
public void onBase64Received(String base64) {
WebsocketReply wr = new WebsocketReply("GET BASE64", "data:image/jpeg;base64,"+ base64);
webServer.SendtoAll(wr);
}
@Override
public void onStreamingStatusReceived(String status) {
WebsocketReply wr = new WebsocketReply("STREAMING STATUS", status);
webServer.SendtoAll(wr);
}
};
rtspGrabber.Start(true, 1920, 1080, re);
rtspGrabber.Start(true, 1920, 1080);
} else Logger.error("Invalid Camera Path");
} else Logger.error("Invalid Camera IP");
}
@@ -276,25 +251,17 @@ public class Main {
// Live Streaming Related Commands
case "GET BASE64":
if (rtspGrabber!=null){
return new WebsocketReply("GET BASE64", "data:image/jpeg;base64,"+ rtspGrabber.getLastBase64());
if (Objects.equals(command.data,"HQ"))
return new WebsocketReply("GET BASE64", "data:image/jpeg;base64,"+ rtspGrabber.getLastHQBase64(), String.format("Streaming at %dx%d", rtspGrabber.getHQWidth(), rtspGrabber.getHQHeight()));
else
return new WebsocketReply("GET BASE64", "data:image/jpeg;base64,"+ rtspGrabber.getLastLQBase64(), String.format("Streaming at %dx%d", rtspGrabber.getLQWidth(), rtspGrabber.getLQHeight()));
} else return new WebsocketReply("GET BASE64", "RTSP Grabber not initialized");
case "GET RESOLUTION":
if (vapixProtocol!=null){
int[] res = vapixProtocol.GetCurrentResolution(1);
return new WebsocketReply("GET RESOLUTION", String.format("%dx%d", res[0], res[1]));
} else return new WebsocketReply("GET RESOLUTION", "VapixProtocol not initialized");
case "SET VIDEO QUALITY":
if (Objects.equals(command.data,"HQ")){
if (rtspGrabber!=null) rtspGrabber.ChangeVideoQuality(true);
return new WebsocketReply("SET VIDEO QUALITY", "High Quality");
} else if (Objects.equals(command.data,"LQ")){
if (rtspGrabber!=null) rtspGrabber.ChangeVideoQuality(false);
return new WebsocketReply("SET VIDEO QUALITY", "Low Quality");
} else return new WebsocketReply("SET VIDEO QUALITY", "Invalid Video Quality");
case "STREAMING STATUS":
if (rtspGrabber!=null){
return new WebsocketReply("STREAMING STATUS", rtspGrabber.GetStreamingStatus());
} else return new WebsocketReply("STREAMING STATUS", "RTSP Grabber not initialized");
default:
return new WebsocketReply("UNKNOWN COMMAND", command.command);
}