forked from j62/ctbrec
1
0
Fork 0

Add providers for MFC streams sources

Since MFC uses different streaming technologies, the stream sources have
to be determined differently. This is now done in dedicated
StreamSourceProvider classes.
This commit is contained in:
0xboobface 2019-12-07 12:00:05 +01:00
parent 98c1731c8e
commit 1c64b82deb
17 changed files with 539 additions and 324 deletions

View File

@ -4,6 +4,8 @@ import java.io.IOException;
import java.util.List;
import java.util.concurrent.ExecutionException;
import javax.xml.bind.JAXBException;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import com.squareup.moshi.JsonReader;
@ -121,7 +123,7 @@ public class JavaFxModel implements Model {
}
@Override
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException {
return delegate.getStreamSources();
}

View File

@ -20,10 +20,12 @@ import ctbrec.ui.controls.Dialogs;
import javafx.scene.Scene;
public class Player {
private static final transient Logger LOG = LoggerFactory.getLogger(Player.class);
private static final Logger LOG = LoggerFactory.getLogger(Player.class);
private static PlayerThread playerThread;
public static Scene scene;
private Player() {}
public static boolean play(String url) {
return play(url, true);
}

View File

@ -22,7 +22,6 @@ import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@ -30,9 +29,6 @@ import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import ctbrec.Config;
import ctbrec.Recording;
import ctbrec.Recording.State;
@ -75,11 +71,12 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Font;
import javafx.stage.FileChooser;
import javafx.util.Callback;
import javafx.util.Duration;
public class RecordingsTab extends Tab implements TabSelectionListener {
private static final transient Logger LOG = LoggerFactory.getLogger(RecordingsTab.class);
private static final String ERROR_WHILE_DOWNLOADING_RECORDING = "Error while downloading recording";
private static final Logger LOG = LoggerFactory.getLogger(RecordingsTab.class);
private ScheduledService<List<JavaFxRecording>> updateService;
private Config config;
@ -91,7 +88,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
FlowPane grid = new FlowPane();
ScrollPane scrollPane = new ScrollPane();
TableView<JavaFxRecording> table = new TableView<JavaFxRecording>();
TableView<JavaFxRecording> table = new TableView<>();
ObservableList<JavaFxRecording> observableRecordings = FXCollections.observableArrayList();
ContextMenu popup;
ProgressBar spaceLeft;
@ -125,104 +122,30 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
name.setPrefWidth(200);
name.setCellValueFactory(cdf -> new SimpleStringProperty(cdf.getValue().getModel().getName()));
TableColumn<JavaFxRecording, Instant> date = new TableColumn<>("Date");
date.setCellValueFactory((cdf) -> {
date.setCellValueFactory(cdf -> {
Instant instant = cdf.getValue().getStartDate();
return new SimpleObjectProperty<Instant>(instant);
});
date.setCellFactory(new Callback<TableColumn<JavaFxRecording, Instant>, TableCell<JavaFxRecording, Instant>>() {
@Override
public TableCell<JavaFxRecording, Instant> call(TableColumn<JavaFxRecording, Instant> param) {
TableCell<JavaFxRecording, Instant> cell = new TableCell<JavaFxRecording, Instant>() {
@Override
protected void updateItem(Instant instant, boolean empty) {
if(empty || instant == null) {
setText(null);
} else {
ZonedDateTime time = instant.atZone(ZoneId.systemDefault());
DateTimeFormatter dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM);
setText(dtf.format(time));
}
}
};
return cell;
}
});
date.setCellFactory(param -> createDateCell());
date.setPrefWidth(200);
TableColumn<JavaFxRecording, String> status = new TableColumn<>("Status");
status.setCellValueFactory((cdf) -> cdf.getValue().getStatusProperty());
status.setCellValueFactory(cdf -> cdf.getValue().getStatusProperty());
status.setPrefWidth(300);
TableColumn<JavaFxRecording, String> progress = new TableColumn<>("Progress");
progress.setCellValueFactory((cdf) -> cdf.getValue().getProgressProperty());
progress.setCellValueFactory(cdf -> cdf.getValue().getProgressProperty());
progress.setPrefWidth(100);
TableColumn<JavaFxRecording, Number> size = new TableColumn<>("Size");
size.setStyle("-fx-alignment: CENTER-RIGHT;");
size.setPrefWidth(100);
size.setCellValueFactory(cdf -> cdf.getValue().getSizeProperty());
size.setCellFactory(new Callback<TableColumn<JavaFxRecording, Number>, TableCell<JavaFxRecording, Number>>() {
@Override
public TableCell<JavaFxRecording, Number> call(TableColumn<JavaFxRecording, Number> param) {
TableCell<JavaFxRecording, Number> cell = new TableCell<JavaFxRecording, Number>() {
@Override
protected void updateItem(Number sizeInByte, boolean empty) {
if(empty || sizeInByte == null) {
setText(null);
setStyle(null);
} else {
setText(StringUtil.formatSize(sizeInByte));
setStyle("-fx-alignment: CENTER-RIGHT;");
if(Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
int row = this.getTableRow().getIndex();
JavaFxRecording rec = tableViewProperty().get().getItems().get(row);
if(!rec.valueChanged() && rec.getStatus() == State.RECORDING) {
setStyle("-fx-alignment: CENTER-RIGHT; -fx-background-color: red");
}
}
}
}
};
return cell;
}
});
size.setCellFactory(param -> createSizeCell());
table.getColumns().addAll(name, date, status, progress, size);
table.setItems(observableRecordings);
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
List<JavaFxRecording> recordings = table.getSelectionModel().getSelectedItems();
if(recordings != null && !recordings.isEmpty()) {
popup = createContextMenu(recordings);
if(!popup.getItems().isEmpty()) {
popup.show(table, event.getScreenX(), event.getScreenY());
}
}
event.consume();
});
table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
if(popup != null) {
popup.hide();
}
});
table.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
if(event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
Recording recording = table.getSelectionModel().getSelectedItem();
if(recording != null) {
play(recording);
}
}
});
table.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
List<JavaFxRecording> recordings = table.getSelectionModel().getSelectedItems();
if (recordings != null && !recordings.isEmpty()) {
if (event.getCode() == KeyCode.DELETE) {
if(recordings.size() > 1 || recordings.get(0).getStatus() == State.FINISHED) {
delete(recordings);
}
} else if (event.getCode() == KeyCode.ENTER) {
if(recordings.get(0).getStatus() == State.FINISHED) {
play(recordings.get(0));
}
}
}
});
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, this::onContextMenuRequested);
table.addEventHandler(MouseEvent.MOUSE_PRESSED, this::onMousePressed);
table.addEventFilter(MouseEvent.MOUSE_CLICKED, this::onMouseClicked);
table.addEventFilter(KeyEvent.KEY_PRESSED, this::onKeyPressed);
scrollPane.setContent(table);
HBox spaceBox = new HBox(5);
@ -246,14 +169,94 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
restoreState();
}
private TableCell<JavaFxRecording, Number> createSizeCell() {
TableCell<JavaFxRecording, Number> cell = new TableCell<JavaFxRecording, Number>() {
@Override
protected void updateItem(Number sizeInByte, boolean empty) {
if(empty || sizeInByte == null) {
setText(null);
setStyle(null);
} else {
setText(StringUtil.formatSize(sizeInByte));
setStyle("-fx-alignment: CENTER-RIGHT;");
if(Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
int row = this.getTableRow().getIndex();
JavaFxRecording rec = tableViewProperty().get().getItems().get(row);
if(!rec.valueChanged() && rec.getStatus() == State.RECORDING) {
setStyle("-fx-alignment: CENTER-RIGHT; -fx-background-color: red");
}
}
}
}
};
return cell;
}
private TableCell<JavaFxRecording, Instant> createDateCell() {
TableCell<JavaFxRecording, Instant> cell = new TableCell<JavaFxRecording, Instant>() {
@Override
protected void updateItem(Instant instant, boolean empty) {
if(empty || instant == null) {
setText(null);
} else {
ZonedDateTime time = instant.atZone(ZoneId.systemDefault());
DateTimeFormatter dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM);
setText(dtf.format(time));
}
}
};
return cell;
}
private void onContextMenuRequested(ContextMenuEvent event) {
List<JavaFxRecording> recordings = table.getSelectionModel().getSelectedItems();
if (recordings != null && !recordings.isEmpty()) {
popup = createContextMenu(recordings);
if (!popup.getItems().isEmpty()) {
popup.show(table, event.getScreenX(), event.getScreenY());
}
}
event.consume();
}
private void onMousePressed(MouseEvent event) {
if(popup != null) {
popup.hide();
}
}
private void onMouseClicked(MouseEvent event) {
if(event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
Recording recording = table.getSelectionModel().getSelectedItem();
if(recording != null) {
play(recording);
}
}
}
private void onKeyPressed( KeyEvent event ) {
List<JavaFxRecording> recordings = table.getSelectionModel().getSelectedItems();
if (recordings != null && !recordings.isEmpty()) {
if (event.getCode() == KeyCode.DELETE) {
if(recordings.size() > 1 || recordings.get(0).getStatus() == State.FINISHED) {
delete(recordings);
}
} else if (event.getCode() == KeyCode.ENTER && recordings.get(0).getStatus() == State.FINISHED) {
play(recordings.get(0));
}
}
}
void initializeUpdateService() {
updateService = createUpdateService();
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2)));
updateService.setOnSucceeded((event) -> {
updateService.setOnSucceeded(event -> {
updateRecordingsTable();
updateFreeSpaceDisplay();
});
updateService.setOnFailed((event) -> {
updateService.setOnFailed(event -> {
LOG.info("Couldn't get list of recordings from recorder", event.getSource().getException());
AutosizeAlert autosizeAlert = new AutosizeAlert(AlertType.ERROR, getTabPane().getScene());
autosizeAlert.setTitle("Whoopsie!");
@ -309,12 +312,12 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
}
private ScheduledService<List<JavaFxRecording>> createUpdateService() {
ScheduledService<List<JavaFxRecording>> updateService = new ScheduledService<List<JavaFxRecording>>() {
ScheduledService<List<JavaFxRecording>> service = new ScheduledService<List<JavaFxRecording>>() {
@Override
protected Task<List<JavaFxRecording>> createTask() {
return new Task<List<JavaFxRecording>>() {
@Override
public List<JavaFxRecording> call() throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
public List<JavaFxRecording> call() throws IOException, InvalidKeyException, NoSuchAlgorithmException {
updateSpace();
List<JavaFxRecording> recordings = new ArrayList<>();
@ -339,17 +342,14 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
};
}
};
ExecutorService executor = Executors.newSingleThreadExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
t.setName("RecordingsTab UpdateService");
return t;
}
ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r);
t.setDaemon(true);
t.setName("RecordingsTab UpdateService");
return t;
});
updateService.setExecutor(executor);
return updateService;
service.setExecutor(executor);
return service;
}
@Override
@ -375,9 +375,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
JavaFxRecording first = recordings.get(0);
MenuItem openInPlayer = new MenuItem("Open in Player");
openInPlayer.setOnAction((e) -> {
play(recordings.get(0));
});
openInPlayer.setOnAction(e -> play(recordings.get(0)));
if(first.getStatus() == State.FINISHED || Config.getInstance().getSettings().localRecording) {
contextMenu.getItems().add(openInPlayer);
}
@ -397,33 +395,24 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
// }
MenuItem deleteRecording = new MenuItem("Delete");
deleteRecording.setOnAction((e) -> {
delete(recordings);
});
deleteRecording.setOnAction(e -> delete(recordings));
if(first.getStatus() == State.FINISHED || first.getStatus() == State.WAITING || first.getStatus() == State.FAILED || recordings.size() > 1) {
contextMenu.getItems().add(deleteRecording);
}
MenuItem openDir = new MenuItem("Open directory");
openDir.setOnAction((e) -> {
String recordingsDir = Config.getInstance().getSettings().recordingsDir;
String path = first.getPath();
File tsFile = new File(recordingsDir, path);
new Thread(() -> {
DesktopIntegration.open(tsFile.getParent());
}).start();
});
openDir.setOnAction(e -> onOpenDirectory(first));
if(Config.getInstance().getSettings().localRecording) {
contextMenu.getItems().add(openDir);
}
MenuItem downloadRecording = new MenuItem("Download");
downloadRecording.setOnAction((e) -> {
downloadRecording.setOnAction(e -> {
try {
download(first);
} catch (IOException | ParseException | PlaylistException e1) {
showErrorDialog("Error while downloading recording", "The recording could not be downloaded", e1);
LOG.error("Error while downloading recording", e1);
} catch (IOException e1) {
showErrorDialog(ERROR_WHILE_DOWNLOADING_RECORDING, "The recording could not be downloaded", e1);
LOG.error(ERROR_WHILE_DOWNLOADING_RECORDING, e1);
}
});
if (!Config.getInstance().getSettings().localRecording && first.getStatus() == State.FINISHED) {
@ -431,9 +420,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
}
MenuItem rerunPostProcessing = new MenuItem("Rerun Post-Processing");
rerunPostProcessing.setOnAction((e) -> {
triggerPostProcessing(first);
});
rerunPostProcessing.setOnAction(e -> triggerPostProcessing(first));
if (first.getStatus() == State.FINISHED || first.getStatus() == State.WAITING) {
contextMenu.getItems().add(rerunPostProcessing);
}
@ -447,6 +434,13 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
return contextMenu;
}
private void onOpenDirectory(JavaFxRecording first) {
String recordingsDir = Config.getInstance().getSettings().recordingsDir;
String path = first.getPath();
File tsFile = new File(recordingsDir, path);
new Thread(() -> DesktopIntegration.open(tsFile.getParent())).start();
}
private void triggerPostProcessing(JavaFxRecording first) {
new Thread(() -> {
try {
@ -458,8 +452,8 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
}).start();
}
private void download(Recording recording) throws IOException, ParseException, PlaylistException {
String filename = recording.getPath().substring(1).replaceAll("/", "-") + ".ts";
private void download(Recording recording) throws IOException {
String filename = recording.getPath().substring(1).replace("/", "-") + ".ts";
FileChooser chooser = new FileChooser();
chooser.setInitialFileName(filename);
if(config.getSettings().lastDownloadDir != null && !config.getSettings().lastDownloadDir.equals("")) {
@ -475,58 +469,52 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
String hlsBase = config.getServerUrl() + "/hls";
URL url = new URL(hlsBase + recording.getPath() + "/playlist.m3u8");
LOG.info("Downloading {}", recording.getPath());
Thread t = new Thread(() -> {
try {
MergedHlsDownload download = new MergedHlsDownload(CamrecApplication.httpClient);
download.start(url.toString(), target, (progress) -> {
Platform.runLater(() -> {
if (progress == 100) {
recording.setStatus(FINISHED);
recording.setProgress(-1);
LOG.debug("Download finished for recording {}", recording.getPath());
} else {
recording.setStatus(DOWNLOADING);
recording.setProgress(progress);
}
});
});
} catch (FileNotFoundException e) {
showErrorDialog("Error while downloading recording", "The target file couldn't be created", e);
LOG.error("Error while downloading recording", e);
} catch (IOException e) {
showErrorDialog("Error while downloading recording", "The recording could not be downloaded", e);
LOG.error("Error while downloading recording", e);
} finally {
Platform.runLater(new Runnable() {
@Override
public void run() {
recording.setStatus(FINISHED);
recording.setProgress(-1);
}
});
}
});
t.setDaemon(true);
t.setName("Download Thread " + recording.getPath());
t.start();
startDownloadThread(url, target, recording);
recording.setStatus(State.DOWNLOADING);
recording.setProgress(0);
}
}
private void showErrorDialog(final String title, final String msg, final Exception e) {
Platform.runLater(new Runnable() {
@Override
public void run() {
AutosizeAlert autosizeAlert = new AutosizeAlert(AlertType.ERROR, getTabPane().getScene());
autosizeAlert.setTitle(title);
autosizeAlert.setHeaderText(msg);
autosizeAlert.setContentText("An error occured: " + e.getLocalizedMessage());
autosizeAlert.showAndWait();
private void startDownloadThread(URL url, File target, Recording recording) {
Thread t = new Thread(() -> {
try {
MergedHlsDownload download = new MergedHlsDownload(CamrecApplication.httpClient);
download.start(url.toString(), target, progress -> Platform.runLater(() -> {
if (progress == 100) {
recording.setStatus(FINISHED);
recording.setProgress(-1);
LOG.debug("Download finished for recording {}", recording.getPath());
} else {
recording.setStatus(DOWNLOADING);
recording.setProgress(progress);
}
}));
} catch (FileNotFoundException e) {
showErrorDialog(ERROR_WHILE_DOWNLOADING_RECORDING, "The target file couldn't be created", e);
LOG.error(ERROR_WHILE_DOWNLOADING_RECORDING, e);
} catch (IOException e) {
showErrorDialog(ERROR_WHILE_DOWNLOADING_RECORDING, "The recording could not be downloaded", e);
LOG.error(ERROR_WHILE_DOWNLOADING_RECORDING, e);
} finally {
Platform.runLater(() -> {
recording.setStatus(FINISHED);
recording.setProgress(-1);
});
}
});
t.setDaemon(true);
t.setName("Download Thread " + recording.getPath());
t.start();
}
private void showErrorDialog(final String title, final String msg, final Exception e) {
Platform.runLater(() -> {
AutosizeAlert autosizeAlert = new AutosizeAlert(AlertType.ERROR, getTabPane().getScene());
autosizeAlert.setTitle(title);
autosizeAlert.setHeaderText(msg);
autosizeAlert.setContentText("An error occured: " + e.getLocalizedMessage());
autosizeAlert.showAndWait();
});
}
private void play(Recording recording) {
@ -543,7 +531,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
}.start();
} else {
String hlsBase = Config.getInstance().getServerUrl() + "/hls";
url = hlsBase + recording.getPath() + "/playlist.m3u8";
url = hlsBase + recording.getPath() + (recording.getPath().endsWith(".mp4") ? "" : "/playlist.m3u8");
new Thread() {
@Override
public void run() {
@ -559,7 +547,6 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
private void delete(List<JavaFxRecording> recordings) {
table.setCursor(Cursor.WAIT);
String msg;
if(recordings.size() > 1) {
msg = "Delete " + recordings.size() + " recordings for good?";
@ -573,38 +560,39 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
confirm.setContentText("");
confirm.showAndWait();
if (confirm.getResult() == ButtonType.YES) {
Thread deleteThread = new Thread() {
@Override
public void run() {
recordingsLock.lock();
try {
List<Recording> deleted = new ArrayList<>();
for (Iterator<JavaFxRecording> iterator = recordings.iterator(); iterator.hasNext();) {
JavaFxRecording r = iterator.next();
if(r.getStatus() != FINISHED) {
continue;
}
try {
recorder.delete(r.getDelegate());
deleted.add(r);
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
LOG.error("Error while deleting recording", e1);
showErrorDialog("Error while deleting recording", "Recording not deleted", e1);
}
}
observableRecordings.removeAll(deleted);
} finally {
recordingsLock.unlock();
Platform.runLater(() -> table.setCursor(Cursor.DEFAULT));
}
}
};
deleteThread.start();
deleteAsync(recordings);
} else {
table.setCursor(Cursor.DEFAULT);
}
}
private void deleteAsync(List<JavaFxRecording> recordings) {
Thread deleteThread = new Thread(() -> {
recordingsLock.lock();
try {
List<Recording> deleted = new ArrayList<>();
for (Iterator<JavaFxRecording> iterator = recordings.iterator(); iterator.hasNext();) {
JavaFxRecording r = iterator.next();
if(r.getStatus() != FINISHED && r.getStatus() != FAILED) {
continue;
}
try {
recorder.delete(r.getDelegate());
deleted.add(r);
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
LOG.error("Error while deleting recording", e1);
showErrorDialog("Error while deleting recording", "Recording not deleted", e1);
}
}
observableRecordings.removeAll(deleted);
} finally {
recordingsLock.unlock();
Platform.runLater(() -> table.setCursor(Cursor.DEFAULT));
}
});
deleteThread.start();
}
public void saveState() {
if(!table.getSortOrder().isEmpty()) {
TableColumn<JavaFxRecording, ?> col = table.getSortOrder().get(0);
@ -616,7 +604,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
columnWidths[i] = table.getColumns().get(i).getWidth();
}
Config.getInstance().getSettings().recordingsColumnWidths = columnWidths;
};
}
private void restoreState() {
String sortCol = Config.getInstance().getSettings().recordingsSortColumn;

View File

@ -3,10 +3,13 @@ package ctbrec.ui.sites.myfreecams;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import ctbrec.Model;
import ctbrec.sites.mfc.MyFreeCamsClient;
import ctbrec.sites.mfc.SessionState;
import ctbrec.sites.mfc.User;
import ctbrec.ui.PaginatedScheduledService;
import javafx.concurrent.Task;
@ -19,10 +22,14 @@ public class HDCamsUpdateService extends PaginatedScheduledService {
public List<Model> call() throws IOException {
MyFreeCamsClient client = MyFreeCamsClient.getInstance();
int modelsPerPage = 50;
return client.getModels().stream()
.filter(m -> m.getPreview() != null)
.filter(m -> m.getStreamUrl() != null)
.filter(m -> m.getStreamUrl().contains("mfc_a_"))
.filter(m -> Optional.ofNullable(client.getSessionState(m))
.map(SessionState::getU)
.map(User::getPhase)
.orElse("").equalsIgnoreCase("a"))
.filter(m -> {
try {
return m.isOnline();
@ -31,7 +38,7 @@ public class HDCamsUpdateService extends PaginatedScheduledService {
}
})
.sorted((m1,m2) -> (int)(m2.getCamScore() - m1.getCamScore()))
.skip( (page-1) * modelsPerPage)
.skip( (page-1l) * modelsPerPage)
.limit(modelsPerPage)
.collect(Collectors.toList());
}

View File

@ -4,6 +4,8 @@ import java.io.IOException;
import java.util.List;
import java.util.concurrent.ExecutionException;
import javax.xml.bind.JAXBException;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import com.squareup.moshi.JsonReader;
@ -73,7 +75,7 @@ public interface Model extends Comparable<Model> {
public State getOnlineState(boolean failFast) throws IOException, ExecutionException;
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException;
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException;
public void invalidateCacheEntries();

View File

@ -56,7 +56,7 @@ public class OS {
configDir = new File(appData, CTBREC);
break;
default:
throw new UnsupportedOperatingSystemException("Unsupported operating system " + System.getProperty(OS_NAME));
throw new UnsupportedOperatingSystemException(System.getProperty(OS_NAME));
}
return configDir;
}
@ -92,7 +92,7 @@ public class OS {
System.arraycopy(args, 0, cmd, 2, args.length);
break;
default:
throw new UnsupportedOperatingSystemException("Unsupported operating system " + System.getProperty(OS_NAME));
throw new UnsupportedOperatingSystemException(System.getProperty(OS_NAME));
}
LOG.debug("Browser command: {}", Arrays.toString(cmd));
return cmd;
@ -101,6 +101,41 @@ public class OS {
}
}
public static String[] getFFmpegCommand(String...args) {
if(System.getenv("CTBREC_FFMPEG") != null) {
String[] cmd = new String[args.length + 1];
cmd[0] = System.getenv("CTBREC_FFMPEG");
System.arraycopy(args, 0, cmd, 1, args.length);
return cmd;
}
try {
URI uri = OS.class.getProtectionDomain().getCodeSource().getLocation().toURI();
File jar = new File(uri.getPath());
File browserDir = new File(jar.getParentFile(), "ffmpeg");
String[] cmd;
switch (getOsType()) {
case LINUX:
case MAC:
cmd = new String[args.length + 1];
cmd[0] = new File(browserDir, "ffmpeg").getAbsolutePath();
System.arraycopy(args, 0, cmd, 1, args.length);
break;
case WINDOWS:
cmd = new String[args.length + 1];
cmd[0] = new File(browserDir, "ffmpeg.exe").getAbsolutePath();
System.arraycopy(args, 0, cmd, 1, args.length);
break;
default:
throw new UnsupportedOperatingSystemException(System.getProperty(OS_NAME));
}
LOG.debug("FFmpeg command: {}", Arrays.toString(cmd));
return cmd;
} catch (URISyntaxException e) {
throw new ForkProcessException(e);
}
}
public static Settings getDefaultSettings() {
Settings settings = new Settings();
if(getOsType() == TYPE.WINDOWS) {

View File

@ -2,8 +2,8 @@ package ctbrec;
public class UnsupportedOperatingSystemException extends RuntimeException {
public UnsupportedOperatingSystemException(String msg) {
super(msg);
public UnsupportedOperatingSystemException(String os) {
super("Unsupported operating system " + os);
}
}

View File

@ -14,6 +14,7 @@ import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
@ -78,30 +79,6 @@ public class DashDownload implements Download {
}
}
private AdaptationSetType chooseBestVideo(List<AdaptationSetType> videoStreams) {
AdaptationSetType best = null;
long bestWidth = 0;
for (AdaptationSetType stream : videoStreams) {
if (stream.getWidth() > bestWidth) {
bestWidth = stream.getWidth();
best = stream;
}
}
return best;
}
private RepresentationType chooseBestRepresentation(List<RepresentationType> representations) {
RepresentationType best = null;
long bestBandwidth = 0;
for (RepresentationType rep : representations) {
if (rep.getBandwidth() > bestBandwidth) {
bestBandwidth = rep.getBandwidth();
best = rep;
}
}
return best;
}
private int downloadSegments(MPDtype mpd, AdaptationSetType adaptationSet, boolean isVideo) throws IOException {
if (adaptationSet == null) {
return 0;
@ -139,6 +116,18 @@ public class DashDownload implements Download {
return downloaded;
}
private RepresentationType chooseBestRepresentation(List<RepresentationType> representations) {
RepresentationType best = null;
long bestBandwidth = 0;
for (RepresentationType rep : representations) {
if (rep.getBandwidth() > bestBandwidth) {
bestBandwidth = rep.getBandwidth();
best = rep;
}
}
return best;
}
private int downloadInitChunksForVideoAndAudio(boolean isVideo, MPDtype mpd, SegmentTemplateType segmentTemplate, RepresentationType representation)
throws IOException {
if (isVideo && !videoInitLoaded || !isVideo && !audioInitLoaded) {
@ -176,11 +165,15 @@ public class DashDownload implements Download {
String absFile = url.getFile();
String prefix = isVideo ? "video" : "audio";
int c = isVideo ? videoCounter++ : audioCounter++;
File targetFile = new File(dir, prefix + '_' + df.format(c) + '_' + new File(absFile).getName());
File segmentFile = new File(dir, prefix + '_' + df.format(c) + '_' + new File(absFile).getName());
while(tries <= 10) {
if (!targetFile.exists() || targetFile.length() == 0) {
LOG.trace("Loading segment, try {}, {} {} {}", tries, response.code(), response.headers().values("Content-Length"), url);
try (FileOutputStream out = new FileOutputStream(targetFile)) {
if (!segmentFile.exists() || segmentFile.length() == 0) {
if (tries > 1) {
LOG.debug("Loading segment, try {}, {} {} {}", tries, response.code(), response.headers().values("Content-Length"), url);
} else {
LOG.trace("Loading segment, try {}, {} {} {}", tries, response.code(), response.headers().values("Content-Length"), url);
}
try (FileOutputStream out = new FileOutputStream(segmentFile)) {
byte[] b = new byte[1024];
int len = -1;
while ((len = in.read(b)) >= 0) {
@ -225,7 +218,7 @@ public class DashDownload implements Download {
}
}
private void downloadManifestAndItsSegments(Unmarshaller u) throws IOException, JAXBException {
private void downloadManifestAndItsSegments(Unmarshaller u) throws IOException, JAXBException, ExecutionException {
String manifest = getManifest(manifestUrl);
LOG.trace("Manifest: {}", manifest);
@SuppressWarnings("unchecked")
@ -249,18 +242,35 @@ public class DashDownload implements Download {
downloadDir.toFile().mkdirs();
AdaptationSetType video = chooseBestVideo(videoStreams);
int downloaded = downloadSegments(mpd, video, true);
if (video == null) {
throw new ExecutionException(new RuntimeException("No stream left in playlist"));
} else {
int downloaded = downloadSegments(mpd, video, true);
AdaptationSetType audio = audioStreams.isEmpty() ? null : audioStreams.get(0);
downloaded += downloadSegments(mpd, audio, false);
AdaptationSetType audio = audioStreams.isEmpty() ? null : audioStreams.get(0);
downloaded += downloadSegments(mpd, audio, false);
if (downloaded == 0) {
LOG.trace("No new segments - Sleeping a bit");
waitSomeTime();
if (downloaded == 0) {
LOG.trace("No new segments - Sleeping a bit");
waitSomeTime();
}
}
}
}
private AdaptationSetType chooseBestVideo(List<AdaptationSetType> videoStreams) {
AdaptationSetType best = null;
int maxHeight = config.getSettings().maximumResolution;
long bestHeight = 0;
for (AdaptationSetType stream : videoStreams) {
if (stream.getHeight() > bestHeight && (maxHeight == 0 || stream.getHeight() <= maxHeight)) {
bestHeight = stream.getHeight();
best = stream;
}
}
return best;
}
@Override
public void stop() {
if (running) {

View File

@ -10,6 +10,7 @@ import java.util.Arrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.OS;
import ctbrec.io.StreamRedirectThread;
public class FfmpegMuxer {
@ -72,14 +73,13 @@ public class FfmpegMuxer {
try {
LOG.debug("Merging:\n{}\n{}\n{}", mp4VideoTrack, mp4AudioTrack, output);
// @formatter:off
String[] cmdline = new String[] {
"ffmpeg",
String[] cmdline = OS.getFFmpegCommand(
"-i", mp4VideoTrack.getCanonicalPath(),
"-i", mp4AudioTrack.getCanonicalPath(),
"-c:v", "copy",
"-c:a", "copy",
output.getCanonicalPath()
};
);
// @formatter:on
LOG.debug("Command line: {}", Arrays.toString(cmdline));
Process ffmpeg = Runtime.getRuntime().exec(cmdline);

View File

@ -19,6 +19,8 @@ import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.xml.bind.JAXBException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -129,7 +131,7 @@ public abstract class AbstractHlsDownload implements Download {
}
protected String getSegmentPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException {
protected String getSegmentPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException {
LOG.debug("{} stream idx: {}", model.getName(), model.getStreamUrlIndex());
List<StreamSource> streamSources = model.getStreamSources();
Collections.sort(streamSources);

View File

@ -4,6 +4,8 @@ import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import javax.xml.bind.JAXBException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -41,7 +43,7 @@ public class LiveJasminHlsDownload extends HlsDownload {
try {
LOG.debug("Updating segment playlist URL for {}", getModel());
segmentUrl = getSegmentPlaylistUrl(getModel());
} catch (IOException | ExecutionException | ParseException | PlaylistException e) {
} catch (IOException | JAXBException | ExecutionException | ParseException | PlaylistException e) {
LOG.error("Couldn't update segment playlist url. This might cause a premature download termination", e);
}
}

View File

@ -4,6 +4,8 @@ import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import javax.xml.bind.JAXBException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -41,7 +43,7 @@ public class LiveJasminMergedHlsDownload extends MergedHlsDownload {
try {
LOG.debug("Updating segment playlist URL for {}", getModel());
segmentUrl = getSegmentPlaylistUrl(getModel());
} catch (IOException | ExecutionException | ParseException | PlaylistException e) {
} catch (IOException | JAXBException | ExecutionException | ParseException | PlaylistException e) {
LOG.error("Couldn't update segment playlist url. This might cause a premature download termination", e);
}
}

View File

@ -0,0 +1,91 @@
package ctbrec.sites.mfc;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.recorder.download.StreamSource;
import ctbrec.recorder.download.dash.AdaptationSetType;
import ctbrec.recorder.download.dash.MPDtype;
import ctbrec.recorder.download.dash.PeriodType;
import ctbrec.recorder.download.dash.RepresentationType;
import ctbrec.sites.Site;
import okhttp3.Request;
import okhttp3.Response;
public class DashStreamSourceProvider implements StreamSourceProvider {
private static final Logger LOG = LoggerFactory.getLogger(DashStreamSourceProvider.class);
private Config config;
private Site site;
public DashStreamSourceProvider(Config config, Site site) {
this.config = config;
this.site = site;
}
@Override
public List<StreamSource> getStreamSources(String streamUrl) throws JAXBException, IOException {
String manifest = getManifest(streamUrl);
JAXBContext jc = JAXBContext.newInstance(MPDtype.class.getPackage().getName());
Unmarshaller u = jc.createUnmarshaller();
@SuppressWarnings("unchecked")
JAXBElement<MPDtype> root = (JAXBElement<MPDtype>) u.unmarshal(new ByteArrayInputStream(manifest.getBytes()));
MPDtype mpd = root.getValue();
List<PeriodType> periods = mpd.getPeriod();
if (periods.isEmpty()) {
return Collections.emptyList();
} else {
PeriodType period = periods.get(0);
List<AdaptationSetType> videoStreams = new ArrayList<>();
List<AdaptationSetType> adaptationSets = period.getAdaptationSet();
for (AdaptationSetType adaptationSet : adaptationSets) {
String mimeType = adaptationSet.getMimeType();
if (mimeType.equalsIgnoreCase("video/mp4")) {
videoStreams.add(adaptationSet);
}
}
return videoStreams.stream().map(ast -> {
RepresentationType representation = ast.getRepresentation().get(0);
StreamSource src = new StreamSource();
src.width = ast.getWidth().intValue();
src.height = ast.getHeight().intValue();
src.bandwidth = (int)representation.getBandwidth();
src.mediaPlaylistUrl = streamUrl;
return src;
}).collect(Collectors.toList());
}
}
private String getManifest(String url) throws IOException {
// @formatter:off
Request request = new Request.Builder()
.url(url)
.header("Accept", "*/*")
.header("Accept-Language", "en-US,en;q=0.5")
.header("User-Agent", config.getSettings().httpUserAgent)
.header("Origin", site.getBaseUrl())
.header("Referer", site.getBaseUrl())
.header("Connection", "keep-alive")
.build(); // @formatter:on
LOG.trace("Loading manifest {}", url);
try (Response response = site.getHttpClient().execute(request)) {
return response.body().string();
}
}
}

View File

@ -0,0 +1,89 @@
package ctbrec.sites.mfc;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.iheartradio.m3u8.Encoding;
import com.iheartradio.m3u8.Format;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.ParsingMode;
import com.iheartradio.m3u8.PlaylistException;
import com.iheartradio.m3u8.PlaylistParser;
import com.iheartradio.m3u8.data.MasterPlaylist;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.PlaylistData;
import ctbrec.Config;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource;
import okhttp3.Request;
import okhttp3.Response;
public class HlsStreamSourceProvider implements StreamSourceProvider {
private static final Logger LOG = LoggerFactory.getLogger(HlsStreamSourceProvider.class);
private HttpClient httpClient;
public HlsStreamSourceProvider(HttpClient httpClient) {
this.httpClient = httpClient;
}
@Override
public List<StreamSource> getStreamSources(String streamUrl) throws IOException, ExecutionException, ParseException, PlaylistException {
MasterPlaylist masterPlaylist = getMasterPlaylist(streamUrl);
List<StreamSource> sources = new ArrayList<>();
for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
if (playlist.hasStreamInfo()) {
StreamSource src = new StreamSource();
src.bandwidth = playlist.getStreamInfo().getBandwidth();
if(playlist.getStreamInfo().getResolution() != null) {
src.width = playlist.getStreamInfo().getResolution().width;
src.height = playlist.getStreamInfo().getResolution().height;
} else {
src.width = Integer.MAX_VALUE;
src.height = Integer.MAX_VALUE;
}
String masterUrl = streamUrl;
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
String segmentUri = baseUrl + playlist.getUri();
src.mediaPlaylistUrl = segmentUri;
LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
sources.add(src);
}
}
if(Config.getInstance().getSettings().mfcIgnoreUpscaled) {
return sources.stream()
.filter(src -> src.height != 960)
.collect(Collectors.toList());
} else {
return sources;
}
}
private MasterPlaylist getMasterPlaylist(String streamUrl) throws IOException, ParseException, PlaylistException {
if(streamUrl == null) {
throw new IllegalStateException("Stream url unknown");
}
LOG.trace("Loading master playlist {}", streamUrl);
Request req = new Request.Builder().url(streamUrl).build();
try(Response response = httpClient.execute(req)) {
if(response.isSuccessful()) {
InputStream inputStream = response.body().byteStream();
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
Playlist playlist = parser.parse();
MasterPlaylist master = playlist.getMasterPlaylist();
return master;
} else {
throw new HttpException(response.code(), response.message());
}
}
}
}

View File

@ -625,20 +625,13 @@ public class MyFreeCamsClient {
return models.getIfPresent(uid);
}
public void getSessionState(ctbrec.Model model) {
public SessionState getSessionState(ctbrec.Model model) {
for (SessionState state : sessionStates.asMap().values()) {
if (Objects.equals(state.getNm(), model.getName())) {
JsonAdapter<SessionState> adapter = moshi.adapter(SessionState.class).indent(" ");
System.out.println(adapter.toJson(state));
System.out.println(model.getPreview());
System.out.println("H5 " + serverConfig.isOnHtml5VideoServer(state));
System.out.println("NG " + serverConfig.isOnNgServer(state));
System.out.println("WZ " + serverConfig.isOnWzObsVideoServer(state));
System.out.println("OBS " + serverConfig.isOnObsServer(state));
System.out.println("URL: " + getStreamUrl(state));
System.out.println("#####################");
return state;
}
}
return null;
}
public ServerConfig getServerConfig() {

View File

@ -1,7 +1,6 @@
package ctbrec.sites.mfc;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
@ -10,21 +9,15 @@ import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import javax.xml.bind.JAXBException;
import org.jsoup.nodes.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.iheartradio.m3u8.Encoding;
import com.iheartradio.m3u8.Format;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.ParsingMode;
import com.iheartradio.m3u8.PlaylistException;
import com.iheartradio.m3u8.PlaylistParser;
import com.iheartradio.m3u8.data.MasterPlaylist;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.PlaylistData;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
@ -98,57 +91,23 @@ public class MyFreeCamsModel extends AbstractModel {
}
@Override
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
MasterPlaylist masterPlaylist = getMasterPlaylist();
List<StreamSource> sources = new ArrayList<>();
for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
if (playlist.hasStreamInfo()) {
StreamSource src = new StreamSource();
src.bandwidth = playlist.getStreamInfo().getBandwidth();
if(playlist.getStreamInfo().getResolution() != null) {
src.width = playlist.getStreamInfo().getResolution().width;
src.height = playlist.getStreamInfo().getResolution().height;
} else {
src.width = Integer.MAX_VALUE;
src.height = Integer.MAX_VALUE;
}
String masterUrl = streamUrl;
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
String segmentUri = baseUrl + playlist.getUri();
src.mediaPlaylistUrl = segmentUri;
LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
sources.add(src);
}
}
if(Config.getInstance().getSettings().mfcIgnoreUpscaled) {
return sources.stream()
.filter(src -> src.height != 960)
.collect(Collectors.toList());
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException {
return getStreamSourceProvider().getStreamSources(updateStreamUrl());
}
private StreamSourceProvider getStreamSourceProvider() {
if(isHlsStream()) {
return new HlsStreamSourceProvider(getSite().getHttpClient());
} else {
return sources;
return new DashStreamSourceProvider(Config.getInstance(), site);
}
}
private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException {
if(getHlsUrl() == null) {
throw new IllegalStateException("Stream url unknown");
}
LOG.trace("Loading master playlist {}", streamUrl);
Request req = new Request.Builder().url(streamUrl).build();
try(Response response = site.getHttpClient().execute(req)) {
if(response.isSuccessful()) {
InputStream inputStream = response.body().byteStream();
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
Playlist playlist = parser.parse();
MasterPlaylist master = playlist.getMasterPlaylist();
return master;
} else {
throw new HttpException(response.code(), response.message());
}
}
private boolean isHlsStream() {
return Optional.ofNullable(streamUrl).orElse("").endsWith("m3u8");
}
private String getHlsUrl() {
private String updateStreamUrl() {
if(streamUrl == null) {
MyFreeCams mfc = (MyFreeCams) getSite();
mfc.getClient().update(this);
@ -211,7 +170,7 @@ public class MyFreeCamsModel extends AbstractModel {
Collections.sort(streamSources);
StreamSource best = streamSources.get(streamSources.size() - 1);
resolution = new int[] { best.width, best.height };
} catch (ParseException | PlaylistException e) {
} catch (JAXBException | ParseException | PlaylistException e) {
LOG.warn("Couldn't determine stream resolution - {}", e.getMessage());
} catch (ExecutionException | IOException e) {
LOG.error("Couldn't determine stream resolution", e);
@ -294,25 +253,38 @@ public class MyFreeCamsModel extends AbstractModel {
int camserv = state.getU().getCamserv();
String server;
ServerConfig sc = ((MyFreeCams)site).getClient().getServerConfig();
String phase = state.getU().getPhase();
if(sc.isOnNgServer(state)) {
server = sc.ngVideoServers.get(Integer.toString(camserv));
camserv = Integer.parseInt(server.replaceAll("[^0-9]+", ""));
previewUrl = "https://snap.mfcimg.com/snapimg/" + camserv + "/320x240/mfc_" + state.getU().getPhase()+ '_' + userChannel;
camserv = toCamServ(server);
previewUrl = toPreviewUrl(camserv, phase, userChannel);
} else if(sc.isOnWzObsVideoServer(state)) {
server = sc.wzobsServers.get(Integer.toString(camserv));
camserv = Integer.parseInt(server.replaceAll("[^0-9]+", ""));
previewUrl = "https://snap.mfcimg.com/snapimg/" + camserv + "/320x240/mfc_" + state.getU().getPhase()+ '_' + userChannel;
camserv = toCamServ(server);
previewUrl = toPreviewUrl(camserv, phase, userChannel);
} else if(sc.isOnHtml5VideoServer(state)) {
server = sc.h5Servers.get(Integer.toString(camserv));
camserv = Integer.parseInt(server.replaceAll("[^0-9]+", ""));
previewUrl = "https://snap.mfcimg.com/snapimg/" + camserv + "/320x240/mfc_" + userChannel;
camserv = toCamServ(server);
previewUrl = toPreviewUrl(camserv, userChannel);
} else {
if(camserv > 500) camserv -= 500;
previewUrl = "https://snap.mfcimg.com/snapimg/" + camserv + "/320x240/mfc_" + userChannel;
previewUrl = toPreviewUrl(camserv, userChannel);
}
return previewUrl;
}
private String toPreviewUrl(int camserv, String phase, int userChannel) {
return "https://snap.mfcimg.com/snapimg/" + camserv + "/320x240/mfc_" + phase + '_' + userChannel;
}
private String toPreviewUrl(int camserv, int userChannel) {
return "https://snap.mfcimg.com/snapimg/" + camserv + "/320x240/mfc_" + userChannel;
}
private int toCamServ(String server) {
return Integer.parseInt(server.replaceAll("[^0-9]+", ""));
}
@Override
public boolean follow() {
return ((MyFreeCams)site).getClient().follow(getUid());

View File

@ -0,0 +1,18 @@
package ctbrec.sites.mfc;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ExecutionException;
import javax.xml.bind.JAXBException;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import ctbrec.recorder.download.StreamSource;
public interface StreamSourceProvider {
public List<StreamSource> getStreamSources(String streamUrl) throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException;
}