September 30, 2024
Video streaming between Windows and Android in the same LAN
Nowadays peer to peer (p2p) video streaming become very popular because it’s implemented by industry standard – webrtc. Adding it into the own app is quite easy and when it’s web-based application, but could become very complicated when it’s cross-platform native application. Market offers many different alternative solutions, but when review them in context of integration into the own application typical disadvantages are: ready to use applications which can’t be extended nor embedded, can’t work without some intermediate server or Internet, complex and complicated etc.
As alternative to the pure video streaming could be using VoIP-based solutions, which designed to send audio/video streams in both directions, and actually do the same.
VoIP solutions are based on SIP and RTP protocols. SIP is using for signalling, it negotiates session between 2 endpoints. After negotiation codecs, ports and other details these endpoints starts sending audio/video medias using RTP protocol. The same RTP protocol is using by webrtc, gstreamer and many others.
Initial requirement
Requirements of the solution, explained below, were quite strict – apps should work in local network without intermediate servers and Internet. Sender part works on android device, which captures video from camera and sends to the Receiver part. In the same time receiver should silently accept connections from Sender and start rending video stream.
Using the code
Here is fragment of the implementation receiver app. It’s created as WinForms app and written on C#. Key role plays SIPrix component, which implements SIP client functionality. Also applications form contains few other controls: panel, where to render received video, 2 labels for displaying local and remote side details, button for ending call.
Here is fragment of code, which covers almost all required implementation: initializes SIP component, adds local account with hardcoded local IP address and port, automatically accepts call from remote side (when it’s hasn’t connected yet) or rejects (when call already present), assigns panel for rendering received video.
public partial class FormMain : Form
{
const int LocalPort = 5555;
const string LocalName = "win";
const string WaitConnStr = "Waiting incoming connection...";
uint mLastCallId = Siprix.Module.kInvalidId;
readonly DirectCallModel callModel_ = new();
private void FormMain_Load(object sender, EventArgs e)
{
//Initialize SDK
callModel_.Initialize(this);
//Assign event handlers
callModel_.OnCallIncoming_ += CallModel_OnCallIncoming;
callModel_.OnCallConnected_ += CallModel_OnCallConnected;
callModel_.OnCallTerminated_ += CallModel_OnCallTerminated;
//Add local account
AccData acc_ = new();
acc_.SipServer = "192.168.0.114";//TODO set own IP address
acc_.SipExtension = LocalName;
acc_.TranspPort = LocalPort;
acc_.TranspProtocol = SipTransport.UDP;
acc_.ExpireTime = 0;
callModel_.AddAccount(acc_);
//Set controls labels and visibility
labelLocal.Text = $"{LocalName}@{acc_.SipServer}:{acc_.TranspPort}";
labelRemote.Text = WaitConnStr;
buttonEndCall.Visible = false;
panelVideo.Visible = false;
}
private void CallModel_OnCallIncoming(uint callId, bool withVideo, string hdrFrom, string hdrTo)
{
if (mLastCallId == Siprix.Module.kInvalidId)
{
mLastCallId = callId;
callModel_.Accept(callId);
callModel_.SetVideoWindow(callId, panelVideo.Handle);
labelRemote.Text = hdrFrom;
panelVideo.Visible = true;
}
else
{
callModel_.Reject(callId);
}
}
private void CallModel_OnCallTerminated(uint callId, uint statusCode)
{
if (mLastCallId == callId)
{
labelRemote.Text = WaitConnStr;
buttonEndCall.Visible = false;
panelVideo.Visible = false;
mLastCallId = Siprix.Module.kInvalidId;
}
}
private void CallModel_OnCallConnected(uint callId, bool withVideo)
{
mLastCallId = callId;
callModel_.MuteCam(callId);
buttonEndCall.Visible = true;
panelVideo.Visible = true;
}
private void buttonEndCall_Click(object sender, EventArgs e)
{
callModel_.EndCall(mLastCallId);
}
Now let’s switch to the Android part.
Create new application, import SIPrix component which implements SIP client for Android, add to manifest ‘CAMERA’ and ‘RECORD_AUDIO’ and also add code for requesting these permissions.
Main activity’s initial state contains only edit box, where user enters URI of the receiver app and button ‘StartCall’. When this button pressed app sends SIP INVITE request directly to the receiver app using specified URI in format: name@IP:port, where “IP” – it’s IP address of computer where is running receiver app, “port”, “name” – port number and name, set in the receiver app, which could be almost any.
Here is fragment of code, which covers almost all required implementation: initializes SIP component, adds local account, starts outgoing call. for test purposes added video fragment with preview, which allows to check is local video capturing.
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private LinearLayout callCtrlLayout_;
private EditText destEdit_;
private CallModel callModel_;
private Button startCallBtn_;
private VideoFragment videoFragment_;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.start_call_button).setOnClickListener(v -> onStartCallClick());
findViewById(R.id.end_call_button).setOnClickListener(v -> onEndCallClick());
findViewById(R.id.mute_mic_button).setOnClickListener(v -> onMuteMicClick());
findViewById(R.id.mute_cam_button).setOnClickListener(v -> onMuteCamClick());
startCallBtn_ = findViewById(R.id.start_call_button);
callCtrlLayout_ = findViewById(R.id.call_ctrl_layout);
destEdit_ = findViewById(R.id.dest);
requestsPermissions();
initCallModel();
}
void initCallModel() {
try {
callModel_ = new CallModel(getApplicationContext());
callModel_.initialize();
callModel_.addAccount();
callModel_.setListener(new ModelListener() {
@Override
public void onCallTerminated(int callId, int statusCode){
toggleControlsVisibility(false);
deleteVideoFragment();
}
@Override
public void onCallConnected(int callId, boolean withVideo){
if(videoFragment_ != null)
callModel_.setVideoRenderers(videoFragment_.getRemoteRenderer(), videoFragment_.getPreviewRenderer());
toggleControlsVisibility(true);
}
});
}catch(Exception e) {
Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
void onStartCallClick() {
final String remoteSideUri = destEdit_.getText().toString();
if(remoteSideUri.isEmpty()) {
Toast.makeText(this, "Remote side uri can't be empty",
Toast.LENGTH_SHORT).show();
return;
}
try {
callModel_.startCall(remoteSideUri);
createVideoFragment();
}catch(Exception e) {
Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
Testing created solution
To start call and streaming video enter URI of the receiver, as shown on image below, and tap “StartCall”.
Android app will send INVITE request to the specified URI and when IP:port were correct, receiver app will accept this call and start rendering video.
This solution can be easily scaled/modified in the following directions:
- Send video stream between 2 Windows or Android applications
- Receive multiple video streams from different clients/devices
- Make stream mono/bidirectional with/without audio
- Send stream between devices/applications in different networks (requires intermediate server)
- Configure bitrate/framerate/resolution
References
Theoretical background:
– SIP protocol: https://en.wikipedia.org/wiki/Session_Initiation_Protocol
– RTP protocol: https://en.wikipedia.org/wiki/Real-time_Transport_Protocol
Download the necessary software:
– Download SIPrix SDK: https://www.siprix-voip.com/download/
Supplementary information:
– If you need more information about Android/Windows apps implementation, here you can find a detailed step-by-step guide: https://docs.siprix-voip.com/rst/integration.html#integration-into-existing-project
Source code of the solution: https://docs.siprix-voip.com/dn/p2p_video_stream.zip
