diff --git a/src/main/java/io/github/dsheirer/alias/Alias.java b/src/main/java/io/github/dsheirer/alias/Alias.java index 53abb627..d8554266 100644 --- a/src/main/java/io/github/dsheirer/alias/Alias.java +++ b/src/main/java/io/github/dsheirer/alias/Alias.java @@ -1,23 +1,20 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2025 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2020 Dennis Sheirer - * * - * * This program is free software: you can redistribute it and/or modify - * * it under the terms of the GNU General Public License as published by - * * the Free Software Foundation, either version 3 of the License, or - * * (at your option) any later version. - * * - * * This program is distributed in the hope that it will be useful, - * * but WITHOUT ANY WARRANTY; without even the implied warranty of - * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * * GNU General Public License for more details. - * * - * * You should have received a copy of the GNU General Public License - * * along with this program. If not, see - * * ***************************************************************************** + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** */ package io.github.dsheirer.alias; @@ -30,13 +27,22 @@ import io.github.dsheirer.alias.id.AliasIDType; import io.github.dsheirer.alias.id.broadcast.BroadcastChannel; import io.github.dsheirer.alias.id.priority.Priority; import io.github.dsheirer.alias.id.record.Record; +import io.github.dsheirer.alias.id.talkgroup.StreamAsTalkgroup; +import java.awt.Color; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; @@ -46,13 +52,6 @@ import javafx.util.Callback; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.awt.Color; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import java.util.TreeSet; - /** * Alias provides an aliasing (e.g. name, color, etc) container that is linked to multiple alias identifiers and * provides a corresponding set of actions related to the alias. @@ -74,6 +73,7 @@ public class Alias private StringProperty mName = new SimpleStringProperty(); private ObservableList mAliasIDs = FXCollections.observableArrayList(); private ObservableList mAliasActions = FXCollections.observableArrayList(); + private ObjectProperty mStreamTalkgroupAlias = new SimpleObjectProperty<>(); /** * Constructs an instance and sets the specified name. @@ -180,6 +180,12 @@ public class Alias return mColor; } + @JsonIgnore + public ObjectProperty streamTalkgroupAliasProperty() + { + return mStreamTalkgroupAlias; + } + /** * Icon name property */ @@ -229,6 +235,24 @@ public class Alias mName.set(name); } + /** + * Alias name + */ + @JacksonXmlProperty(isAttribute = true, localName = "stream_talkgroup_alias") + public StreamAsTalkgroup getStreamTalkgroupAlias() + { + return mStreamTalkgroupAlias.get(); + } + + /** + * Sets the stream as talkgroup value that will be used in the TO field for streamed audio calls. + * @param streamTalkgroupAlias with a talkgroup value. + */ + public void setStreamTalkgroupAlias(StreamAsTalkgroup streamTalkgroupAlias) + { + mStreamTalkgroupAlias.set(streamTalkgroupAlias); + } + /** * Alias list name for the alias list that this alias belongs to */ @@ -710,6 +734,7 @@ public class Alias { return (Alias a) -> new Observable[] {a.recordableProperty(), a.streamableProperty(), a.colorProperty(), a.aliasListNameProperty(), a.groupProperty(), a.iconNameProperty(), a.nameProperty(), a.aliasIds(), - a.aliasActions(), a.nonAudioIdentifierCountProperty(), a.overlapProperty()}; + a.aliasActions(), a.nonAudioIdentifierCountProperty(), a.overlapProperty(), a.priorityProperty(), + a.streamTalkgroupAliasProperty()}; } } diff --git a/src/main/java/io/github/dsheirer/alias/id/AliasID.java b/src/main/java/io/github/dsheirer/alias/id/AliasID.java index 79539e35..79e79f06 100644 --- a/src/main/java/io/github/dsheirer/alias/id/AliasID.java +++ b/src/main/java/io/github/dsheirer/alias/id/AliasID.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2025 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -42,6 +42,7 @@ import io.github.dsheirer.alias.id.record.Record; import io.github.dsheirer.alias.id.status.UnitStatusID; import io.github.dsheirer.alias.id.status.UserStatusID; import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup; +import io.github.dsheirer.alias.id.talkgroup.StreamAsTalkgroup; import io.github.dsheirer.alias.id.talkgroup.Talkgroup; import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange; import io.github.dsheirer.alias.id.tone.TonesID; @@ -70,6 +71,7 @@ import javafx.util.Callback; @JsonSubTypes.Type(value = RadioRange.class, name = "radioRange"), @JsonSubTypes.Type(value = Record.class, name = "record"), @JsonSubTypes.Type(value = SiteID.class, name = "siteID"), + @JsonSubTypes.Type(value = StreamAsTalkgroup.class, name = "streamAsTalkgroup"), @JsonSubTypes.Type(value = Talkgroup.class, name = "talkgroup"), @JsonSubTypes.Type(value = TalkgroupRange.class, name = "talkgroupRange"), @JsonSubTypes.Type(value = TonesID.class, name = "tones"), diff --git a/src/main/java/io/github/dsheirer/alias/id/talkgroup/StreamAsTalkgroup.java b/src/main/java/io/github/dsheirer/alias/id/talkgroup/StreamAsTalkgroup.java new file mode 100644 index 00000000..97380ea2 --- /dev/null +++ b/src/main/java/io/github/dsheirer/alias/id/talkgroup/StreamAsTalkgroup.java @@ -0,0 +1,40 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2025 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.alias.id.talkgroup; + +import io.github.dsheirer.protocol.Protocol; + +/** + * Streaming talkgroup alias. For streamed audio metadata, this value is used to replace the decoded talkgroup or TO + * value with an alias value when there are talkgroup collisions resulting from streaming multiple channels to the + * same stream. This is common in P25 conventional repeater setups where each radio channel is using talkgroup 1. + */ +public class StreamAsTalkgroup extends Talkgroup +{ + public StreamAsTalkgroup(int talkgroup) + { + super(Protocol.UNKNOWN, talkgroup); + } + + public StreamAsTalkgroup() + { + //No arg JAXB constructor + } +} diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/broadcastify/BroadcastifyCallBroadcaster.java b/src/main/java/io/github/dsheirer/audio/broadcast/broadcastify/BroadcastifyCallBroadcaster.java index 442092bf..43ffa801 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/broadcastify/BroadcastifyCallBroadcaster.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/broadcastify/BroadcastifyCallBroadcaster.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2025 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,6 +20,8 @@ package io.github.dsheirer.audio.broadcast.broadcastify; import com.google.common.net.HttpHeaders; +import io.github.dsheirer.alias.Alias; +import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.alias.AliasModel; import io.github.dsheirer.audio.broadcast.AbstractAudioBroadcaster; import io.github.dsheirer.audio.broadcast.AudioRecording; @@ -32,6 +34,7 @@ import io.github.dsheirer.identifier.Form; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.IdentifierClass; import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.configuration.AliasListConfigurationIdentifier; import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier; import io.github.dsheirer.identifier.patch.PatchGroup; import io.github.dsheirer.identifier.patch.PatchGroupIdentifier; @@ -45,6 +48,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; +import java.util.List; import java.util.Queue; import java.util.concurrent.CompletionException; import java.util.concurrent.LinkedTransferQueue; @@ -76,6 +80,7 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster aliases = aliasList.getAliases(identifier); + + for(Alias a: aliases) + { + if(a.getStreamTalkgroupAlias() != null) + { + return String.valueOf(a.getStreamTalkgroupAlias().getValue()); + } + } + } + } + } + if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier) { return format(patchGroupIdentifier); diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastMetadata.java b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastMetadata.java index 6fa78fc3..0ebd6d8c 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastMetadata.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/icecast/IcecastMetadata.java @@ -1,23 +1,20 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2025 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2020 Dennis Sheirer - * * - * * This program is free software: you can redistribute it and/or modify - * * it under the terms of the GNU General Public License as published by - * * the Free Software Foundation, either version 3 of the License, or - * * (at your option) any later version. - * * - * * This program is distributed in the hope that it will be useful, - * * but WITHOUT ANY WARRANTY; without even the implied warranty of - * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * * GNU General Public License for more details. - * * - * * You should have received a copy of the GNU General Public License - * * along with this program. If not, see - * * ***************************************************************************** + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** */ package io.github.dsheirer.audio.broadcast.icecast; @@ -25,21 +22,16 @@ import com.google.common.base.Joiner; import io.github.dsheirer.alias.Alias; import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.alias.AliasModel; -import io.github.dsheirer.audio.broadcast.icecast.IcecastConfiguration; import io.github.dsheirer.identifier.Form; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.IdentifierClass; import io.github.dsheirer.identifier.IdentifierCollection; import io.github.dsheirer.identifier.Role; -import io.github.dsheirer.properties.SystemProperties; -import io.github.dsheirer.util.ThreadPool; +import java.util.List; +import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.util.List; - public class IcecastMetadata { private final static Logger mLog = LoggerFactory.getLogger(IcecastMetadata.class); @@ -73,13 +65,23 @@ public class IcecastMetadata if(to != null) { - sb.append("TO:").append(to); - List aliases = aliasList.getAliases(to); - if(aliases != null && !aliases.isEmpty()) + //Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value. + Optional streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst(); + + if(streamAs.isPresent()) { - sb.append(" ").append(Joiner.on(", ").skipNulls().join(aliases)); + sb.append("TO:").append(streamAs.get().getStreamTalkgroupAlias().getValue()); + } + else + { + sb.append("TO:").append(to); + + if(!aliases.isEmpty()) + { + sb.append(" ").append(Joiner.on(", ").skipNulls().join(aliases)); + } } } else diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/openmhz/OpenMHzBroadcaster.java b/src/main/java/io/github/dsheirer/audio/broadcast/openmhz/OpenMHzBroadcaster.java index 8337a6aa..7a2f8bc7 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/openmhz/OpenMHzBroadcaster.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/openmhz/OpenMHzBroadcaster.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2025 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,56 +19,45 @@ package io.github.dsheirer.audio.broadcast.openmhz; - import com.google.common.net.HttpHeaders; - import com.google.common.base.Joiner; - import io.github.dsheirer.alias.Alias; - import io.github.dsheirer.alias.AliasList; - import io.github.dsheirer.alias.AliasModel; - import io.github.dsheirer.audio.broadcast.AbstractAudioBroadcaster; - import io.github.dsheirer.audio.broadcast.AudioRecording; - import io.github.dsheirer.audio.broadcast.BroadcastEvent; - import io.github.dsheirer.audio.broadcast.BroadcastState; - import io.github.dsheirer.audio.convert.InputAudioFormat; - import io.github.dsheirer.audio.convert.MP3Setting; - import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder; - import io.github.dsheirer.identifier.Form; - import io.github.dsheirer.identifier.Identifier; - import io.github.dsheirer.identifier.IdentifierClass; - import io.github.dsheirer.identifier.MutableIdentifierCollection; - import io.github.dsheirer.identifier.Role; - import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier; - import io.github.dsheirer.identifier.patch.PatchGroup; - import io.github.dsheirer.identifier.patch.PatchGroupIdentifier; - import io.github.dsheirer.identifier.radio.RadioIdentifier; - import io.github.dsheirer.identifier.alias.TalkerAliasIdentifier; - import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; - import io.github.dsheirer.util.ThreadPool; - import org.slf4j.Logger; - import org.slf4j.LoggerFactory; - - import java.io.File; - import java.nio.file.Files; - import java.io.FileInputStream; - import java.io.FileNotFoundException; - import java.io.IOException; - import java.io.PrintWriter; - import java.io.BufferedReader; - import java.io.InputStreamReader; - import java.io.OutputStreamWriter; - import java.net.ConnectException; - import java.net.URI; - import java.net.URL; - import java.net.URLConnection; - import java.net.http.HttpClient; - import java.net.http.HttpRequest; - import java.net.http.HttpResponse; - import java.time.Duration; - import java.util.List; - import java.util.Queue; - import java.util.concurrent.CompletionException; - import java.util.concurrent.LinkedTransferQueue; - import java.util.concurrent.ScheduledFuture; - import java.util.concurrent.TimeUnit; +import com.google.common.net.HttpHeaders; +import io.github.dsheirer.alias.Alias; +import io.github.dsheirer.alias.AliasList; +import io.github.dsheirer.alias.AliasModel; +import io.github.dsheirer.audio.broadcast.AbstractAudioBroadcaster; +import io.github.dsheirer.audio.broadcast.AudioRecording; +import io.github.dsheirer.audio.broadcast.BroadcastEvent; +import io.github.dsheirer.audio.broadcast.BroadcastState; +import io.github.dsheirer.audio.convert.InputAudioFormat; +import io.github.dsheirer.audio.convert.MP3Setting; +import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder; +import io.github.dsheirer.identifier.Form; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.IdentifierClass; +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.alias.TalkerAliasIdentifier; +import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier; +import io.github.dsheirer.identifier.patch.PatchGroup; +import io.github.dsheirer.identifier.patch.PatchGroupIdentifier; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; +import io.github.dsheirer.util.ThreadPool; +import java.io.IOException; +import java.net.ConnectException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import java.util.Queue; +import java.util.concurrent.CompletionException; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @@ -428,22 +417,40 @@ * Creates a formatted string with the TO identifiers or uses a default of zero (0) * */ - private static String getTo(AudioRecording audioRecording) + private String getTo(AudioRecording audioRecording) { Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier(); - if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier) + if(identifier != null) { - return patchGroupIdentifier.getValue().getPatchGroup().getValue().toString(); - } - else if(identifier instanceof TalkgroupIdentifier talkgroupIdentifier) - { - return String.valueOf(RadioReferenceDecoder.convertToRadioReferenceTalkgroup(talkgroupIdentifier.getValue(), - talkgroupIdentifier.getProtocol())); - } - else if(identifier instanceof RadioIdentifier radioIdentifier) - { - return radioIdentifier.getValue().toString(); + AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection()); + + if(aliasList != null) + { + List aliases = aliasList.getAliases(identifier); + + //Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value. + Optional streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst(); + + if(streamAs.isPresent()) + { + return String.valueOf(streamAs.get().getStreamTalkgroupAlias().getValue()); + } + } + + if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier) + { + return patchGroupIdentifier.getValue().getPatchGroup().getValue().toString(); + } + else if(identifier instanceof TalkgroupIdentifier talkgroupIdentifier) + { + return String.valueOf(RadioReferenceDecoder.convertToRadioReferenceTalkgroup(talkgroupIdentifier.getValue(), + talkgroupIdentifier.getProtocol())); + } + else if(identifier instanceof RadioIdentifier radioIdentifier) + { + return radioIdentifier.getValue().toString(); + } } return "0"; diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/rdioscanner/RdioScannerBroadcaster.java b/src/main/java/io/github/dsheirer/audio/broadcast/rdioscanner/RdioScannerBroadcaster.java index fad9571f..468a42dd 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/rdioscanner/RdioScannerBroadcaster.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/rdioscanner/RdioScannerBroadcaster.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2025 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -34,11 +34,11 @@ import io.github.dsheirer.identifier.Form; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.IdentifierClass; import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.alias.TalkerAliasIdentifier; import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier; import io.github.dsheirer.identifier.patch.PatchGroup; import io.github.dsheirer.identifier.patch.PatchGroupIdentifier; import io.github.dsheirer.identifier.radio.RadioIdentifier; -import io.github.dsheirer.identifier.alias.TalkerAliasIdentifier; import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; import io.github.dsheirer.util.ThreadPool; import java.io.IOException; @@ -49,6 +49,7 @@ import java.net.http.HttpResponse; import java.nio.file.Files; import java.time.Duration; import java.util.List; +import java.util.Optional; import java.util.Queue; import java.util.concurrent.CompletionException; import java.util.concurrent.LinkedTransferQueue; @@ -433,22 +434,40 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster aliases = aliasList.getAliases(identifier); + + //Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value. + Optional streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst(); + + if(streamAs.isPresent()) + { + return String.valueOf(streamAs.get().getStreamTalkgroupAlias().getValue()); + } + } + + if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier) + { + return patchGroupIdentifier.getValue().getPatchGroup().getValue().toString(); + } + else if(identifier instanceof TalkgroupIdentifier talkgroupIdentifier) + { + return String.valueOf(RadioReferenceDecoder.convertToRadioReferenceTalkgroup(talkgroupIdentifier.getValue(), + talkgroupIdentifier.getProtocol())); + } + else if(identifier instanceof RadioIdentifier radioIdentifier) + { + return radioIdentifier.getValue().toString(); + } } return "0"; diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v1/ShoutcastV1BroadcastMetadataUpdater.java b/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v1/ShoutcastV1BroadcastMetadataUpdater.java index 541c46f3..c8bf2879 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v1/ShoutcastV1BroadcastMetadataUpdater.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v1/ShoutcastV1BroadcastMetadataUpdater.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2025 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,6 +29,17 @@ import io.github.dsheirer.identifier.IdentifierClass; import io.github.dsheirer.identifier.IdentifierCollection; import io.github.dsheirer.identifier.Role; import io.github.dsheirer.util.ThreadPool; +import java.io.UnsupportedEncodingException; +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Queue; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.mina.core.future.ConnectFuture; import org.apache.mina.core.service.IoHandlerAdapter; import org.apache.mina.core.session.IoSession; @@ -41,17 +52,6 @@ import org.apache.mina.transport.socket.nio.NioSocketConnector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.UnsupportedEncodingException; -import java.net.ConnectException; -import java.net.InetSocketAddress; -import java.net.URLEncoder; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.concurrent.LinkedTransferQueue; -import java.util.concurrent.atomic.AtomicBoolean; - public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUpdater { private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV1BroadcastMetadataUpdater.class); @@ -212,10 +212,20 @@ public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUp if(to != null) { - sb.append("TO:").append(to); - List aliases = aliasList.getAliases(to); + //Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value. + Optional streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst(); + + if(streamAs.isPresent()) + { + sb.append("TO:").append(streamAs.get().getStreamTalkgroupAlias().getValue()); + } + else + { + sb.append("TO:").append(to); + } + if(!aliases.isEmpty()) { sb.append(" ").append(Joiner.on(", ").skipNulls().join(aliases)); diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v2/ShoutcastV2AudioStreamingBroadcaster.java b/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v2/ShoutcastV2AudioStreamingBroadcaster.java index f10fb690..9bf7cee8 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v2/ShoutcastV2AudioStreamingBroadcaster.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/shoutcast/v2/ShoutcastV2AudioStreamingBroadcaster.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2025 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -49,6 +49,15 @@ import io.github.dsheirer.identifier.IdentifierCollection; import io.github.dsheirer.identifier.Role; import io.github.dsheirer.properties.SystemProperties; import io.github.dsheirer.util.ThreadPool; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.math3.util.FastMath; import org.apache.mina.core.RuntimeIoException; import org.apache.mina.core.future.ConnectFuture; @@ -59,15 +68,6 @@ import org.apache.mina.transport.socket.nio.NioSocketConnector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.ConnectException; -import java.net.InetSocketAddress; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.LinkedTransferQueue; -import java.util.concurrent.atomic.AtomicBoolean; - public class ShoutcastV2AudioStreamingBroadcaster extends AudioStreamingBroadcaster implements IBroadcastMetadataUpdater { private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV2AudioStreamingBroadcaster.class); @@ -303,13 +303,23 @@ public class ShoutcastV2AudioStreamingBroadcaster extends AudioStreamingBroadcas if(to != null) { - sbTitle2.append("TO:").append(to); - List aliases = aliasList.getAliases(to); - if(!aliases.isEmpty()) + //Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value. + Optional streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst(); + + if(streamAs.isPresent()) { - sbTitle2.append(" ").append(Joiner.on(", ").skipNulls().join(aliases)); + sbTitle2.append("TO:").append(streamAs.get().getStreamTalkgroupAlias().getValue()); + } + else + { + sbTitle2.append("TO:").append(to); + + if(!aliases.isEmpty()) + { + sbTitle2.append(" ").append(Joiner.on(", ").skipNulls().join(aliases)); + } } } else diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java index b6f3ddb8..70b5a9cf 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java @@ -44,12 +44,14 @@ import io.github.dsheirer.alias.id.radio.RadioRange; import io.github.dsheirer.alias.id.status.UnitStatusID; import io.github.dsheirer.alias.id.status.UserStatusID; import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup; +import io.github.dsheirer.alias.id.talkgroup.StreamAsTalkgroup; import io.github.dsheirer.alias.id.talkgroup.Talkgroup; import io.github.dsheirer.alias.id.talkgroup.TalkgroupFormatter; import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange; import io.github.dsheirer.alias.id.tone.TonesID; import io.github.dsheirer.audio.broadcast.ConfiguredBroadcast; import io.github.dsheirer.eventbus.MyEventBus; +import io.github.dsheirer.gui.control.IntegerFormatter; import io.github.dsheirer.gui.playlist.Editor; import io.github.dsheirer.gui.playlist.alias.action.ActionEditor; import io.github.dsheirer.gui.playlist.alias.action.ActionEditorFactory; @@ -95,7 +97,9 @@ import javafx.scene.control.MenuItem; import javafx.scene.control.ScrollPane; import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.TextField; +import javafx.scene.control.TextFormatter; import javafx.scene.control.TitledPane; +import javafx.scene.control.Tooltip; import javafx.scene.image.ImageView; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; @@ -152,6 +156,8 @@ public class AliasItemEditor extends Editor private Button mDeleteActionButton; private VBox mActionEditorBox; private VBox mIdentifierEditorBox; + private TextField mStreamAsTalkgroupField; + private TextFormatter mStreamAsIntegerTextFormatter = new IntegerFormatter(1,0xFFFF); private Map mIdentifierEditorMap = new HashMap<>(); private EmptyIdentifierEditor mEmptyIdentifierEditor = new EmptyIdentifierEditor(); @@ -358,6 +364,10 @@ public class AliasItemEditor extends Editor alias.addAliasID(selected); } + //Set or clear the 'Stream As Talkgroup' value. + Integer streamAsTalkgroup = mStreamAsIntegerTextFormatter.getValue(); + alias.setStreamTalkgroupAlias(streamAsTalkgroup != null ? new StreamAsTalkgroup(streamAsTalkgroup) : null); + //Remove and replace the remaining non-audio identifiers alias.removeNonAudioIdentifiers(); @@ -366,8 +376,16 @@ public class AliasItemEditor extends Editor //Create a copy of the identifier so that the alias and the editor don't have the same instance //and reset the overlap flag (if set) so that the alias list can reevaluate the overlap state. AliasID copy = AliasFactory.copyOf(aliasID); - copy.setOverlap(false); - alias.addAliasID(copy); + + if(copy != null) + { + copy.setOverlap(false); + alias.addAliasID(copy); + } + else + { + mLog.warn("Unable to create a copy of alias ID: " + aliasID); + } } //Remove and replace alias actions @@ -415,6 +433,29 @@ public class AliasItemEditor extends Editor MyEventBus.getGlobalEventBus().unregister(this); } + /** + * Editor field for the stream this alias as a different talkgroup value + */ + private TextField getStreamAsTalkgroupField() + { + if(mStreamAsTalkgroupField == null) + { + mStreamAsTalkgroupField = new TextField(); + mStreamAsTalkgroupField.setDisable(true); + mStreamAsTalkgroupField.setTextFormatter(mStreamAsIntegerTextFormatter); + mStreamAsTalkgroupField.setTooltip(new Tooltip("When specified, all streamed call audio will use this " + + "talkgroup value in place of the decoded talkgroup value.")); + mStreamAsTalkgroupField.textProperty().addListener((o, old, newV) -> { + if(getItem() != null) + { + modifiedProperty().set(true); + } + }); + } + + return mStreamAsTalkgroupField; + } + private VBox getTitledPanesBox() { if(mTitledPanesBox == null) @@ -792,7 +833,7 @@ public class AliasItemEditor extends Editor VBox selectedBox = new VBox(); VBox.setVgrow(getSelectedStreamsView(), Priority.ALWAYS); - selectedBox.getChildren().addAll(new Label("Selected"),getSelectedStreamsView()); + selectedBox.getChildren().addAll(new Label("Selected"), getSelectedStreamsView()); HBox hbox = new HBox(); hbox.setSpacing(10); @@ -800,7 +841,17 @@ public class AliasItemEditor extends Editor HBox.setHgrow(selectedBox, Priority.ALWAYS); hbox.getChildren().addAll(availableBox, buttonBox, selectedBox); - mStreamPane = new TitledPane("Streaming", hbox); + VBox vbox = new VBox(); + vbox.setSpacing(10); + VBox.setVgrow(hbox, Priority.ALWAYS); + Label label = new Label("Stream this Alias as Talkgroup:"); + HBox streamAsHBox = new HBox(); + streamAsHBox.setAlignment(Pos.CENTER_LEFT); + streamAsHBox.setSpacing(10); + streamAsHBox.getChildren().addAll(label, getStreamAsTalkgroupField()); + vbox.getChildren().addAll(hbox, streamAsHBox); + + mStreamPane = new TitledPane("Streaming", vbox); mStreamPane.setExpanded(false); } @@ -809,33 +860,44 @@ public class AliasItemEditor extends Editor private void updateStreamViews() { - Platform.runLater(new Runnable() - { - @Override - public void run() + Platform.runLater(() -> { + getAvailableStreamsView().getItems().clear(); + getSelectedStreamsView().getItems().clear(); + getAvailableStreamsView().setDisable(getItem() == null); + getSelectedStreamsView().setDisable(getItem() == null); + getStreamAsTalkgroupField().setDisable(getItem() == null); + + if(getItem() != null) { - getAvailableStreamsView().getItems().clear(); - getSelectedStreamsView().getItems().clear(); - getAvailableStreamsView().setDisable(getItem() == null); - getSelectedStreamsView().setDisable(getItem() == null); + List availableStreams = mPlaylistManager.getBroadcastModel().getBroadcastConfigurationNames(); - if(getItem() != null) + Set selectedChannels = getItem().getBroadcastChannels(); + + for(BroadcastChannel channel: selectedChannels) { - List availableStreams = mPlaylistManager.getBroadcastModel().getBroadcastConfigurationNames(); - - Set selectedChannels = getItem().getBroadcastChannels(); - - for(BroadcastChannel channel: selectedChannels) + if(availableStreams.contains(channel.getChannelName())) { - if(availableStreams.contains(channel.getChannelName())) - { - availableStreams.remove(channel.getChannelName()); - } + availableStreams.remove(channel.getChannelName()); } - - getSelectedStreamsView().getItems().addAll(selectedChannels); - getAvailableStreamsView().getItems().addAll(availableStreams); } + + getSelectedStreamsView().getItems().addAll(selectedChannels); + getAvailableStreamsView().getItems().addAll(availableStreams); + + AliasID streamAs = getItem().getStreamTalkgroupAlias(); + + if(streamAs instanceof StreamAsTalkgroup sat) + { + mStreamAsIntegerTextFormatter.setValue(sat.getValue()); + } + else + { + mStreamAsIntegerTextFormatter.setValue(null); + } + } + else + { + mStreamAsIntegerTextFormatter.setValue(null); } }); }