initial commit

Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
ale
2025-06-15 16:19:32 +02:00
commit e8a203a71d
Se han modificado 42 ficheros con 1968 adiciones y 0 borrados

171
android-app/README.md Archivo normal
Ver fichero

@@ -0,0 +1,171 @@
# ChatRTC - Android WebRTC Chat Application
A modern Android chat application with WebRTC support for audio/video calling and emoji text messaging.
## Features
- **Nickname-based joining**: Users only need to enter their nickname to join the chat
- **Text messaging with emoji support**: Full emoji keyboard and rendering support
- **Audio/Video calling**: WebRTC-powered audio and video communication
- **Modern UI**: Material Design components with beautiful chat bubbles
- **Camera controls**: Toggle video, mute audio, and switch between front/back cameras
- **Real-time communication**: Instant messaging and WebRTC peer-to-peer connections
## Project Structure
```
chatrtc/
├── app/ # Android application
│ ├── src/main/java/com/chatrtc/app/
│ │ ├── MainActivity.java # Nickname entry screen
│ │ ├── ChatActivity.java # Main chat interface
│ │ ├── adapter/
│ │ │ └── ChatAdapter.java # RecyclerView adapter for messages
│ │ ├── model/
│ │ │ └── ChatMessage.java # Message data model
│ │ └── webrtc/
│ │ └── WebRTCManager.java # WebRTC connection management
│ └── src/main/res/ # Android resources
└── signaling-server/ # Node.js signaling server
├── server.js # Socket.IO signaling server
└── package.json # Server dependencies
```
## Setup Instructions
### 1. Android App Setup
1. **Prerequisites**:
- Android Studio Arctic Fox or later
- JDK 8 or later
- Android SDK API 24+ (Android 7.0)
2. **Open the project**:
```bash
cd chatrtc
# Open in Android Studio
```
3. **Configure signaling server URL**:
- Open `app/src/main/java/com/chatrtc/app/webrtc/WebRTCManager.java`
- Update the `SIGNALING_SERVER_URL` constant with your server URL:
```java
private static final String SIGNALING_SERVER_URL = "https://your-server.com";
// For local development: "http://10.0.2.2:3000"
```
4. **Build and run**:
- Connect your Android device or start an emulator
- Click "Run" in Android Studio
### 2. Signaling Server Setup
1. **Install Node.js**: Download from [nodejs.org](https://nodejs.org/)
2. **Install dependencies**:
```bash
cd signaling-server
npm install
```
3. **Start the server**:
```bash
npm start
# or for development with auto-restart:
npm run dev
```
4. **Server will run on**: `http://localhost:3000`
## Usage
1. **Start the signaling server** (see setup instructions above)
2. **Install and run the Android app** on your device(s)
3. **Enter your nickname** on the welcome screen
4. **Grant permissions** for camera and microphone when prompted
5. **Start chatting**:
- Send text messages with emoji support
- Toggle video/audio using the control buttons
- Switch cameras using the camera switch button
- Multiple users can join the same chat room
## Key Components
### Android App
- **MainActivity**: Handles nickname entry and permissions
- **ChatActivity**: Main chat interface with video views and message list
- **WebRTCManager**: Manages WebRTC connections, media streams, and signaling
- **ChatAdapter**: Handles different message types (own, other, system)
- **Emoji Support**: Uses Vanniktech emoji library for full emoji rendering
### WebRTC Features
- **Peer-to-peer connections**: Direct audio/video streams between users
- **Data channels**: For text message transmission
- **STUN servers**: Google's STUN servers for NAT traversal
- **Camera management**: Front/back camera switching
- **Media controls**: Toggle audio/video streams
### Signaling Server
- **Socket.IO based**: Real-time bidirectional communication
- **Room management**: Users join a common chat room
- **WebRTC signaling**: Handles offer/answer/ICE candidate exchange
- **User management**: Tracks connected users and broadcasts join/leave events
## Dependencies
### Android
- **WebRTC**: `org.webrtc:google-webrtc:1.0.32006`
- **Socket.IO**: `io.socket:socket.io-client:2.0.1`
- **Emoji Support**: `com.vanniktech:emoji-google:0.16.0`
- **Material Design**: `com.google.android.material:material:1.9.0`
### Server
- **Express**: Web server framework
- **Socket.IO**: Real-time communication
- **CORS**: Cross-origin resource sharing
## Network Configuration
For local development, the app includes network security configuration to allow HTTP connections to localhost and common local IP addresses.
## Permissions
The app requires:
- `CAMERA`: For video calling
- `RECORD_AUDIO`: For audio calling
- `MODIFY_AUDIO_SETTINGS`: For audio management
- `INTERNET`: For signaling server connection
- `ACCESS_NETWORK_STATE`: For network status
## Deployment
### Android App
- Build APK: `./gradlew assembleDebug`
- Install: `adb install app/build/outputs/apk/debug/app-debug.apk`
### Signaling Server
- Deploy to any Node.js hosting service (Heroku, Railway, etc.)
- Update the `SIGNALING_SERVER_URL` in the Android app
- Ensure HTTPS is used for production (required by WebRTC)
## Troubleshooting
1. **Connection issues**: Check signaling server URL and network connectivity
2. **Permission errors**: Ensure camera/microphone permissions are granted
3. **Video not showing**: Check camera availability and permissions
4. **Audio not working**: Verify microphone permissions and device audio settings
## Contributing
1. Fork the repository
2. Create your feature branch
3. Commit your changes
4. Push to the branch
5. Create a Pull Request
## License
This project is open source and available under the MIT License.

61
android-app/app/build.gradle Archivo normal
Ver fichero

@@ -0,0 +1,61 @@
plugins {
id 'com.android.application'
}
android {
namespace 'com.chatrtc.app'
compileSdk 34
defaultConfig {
applicationId "com.chatrtc.app"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildFeatures {
viewBinding true
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.recyclerview:recyclerview:1.3.1'
// WebRTC
implementation 'org.webrtc:google-webrtc:1.0.32006'
// Socket.IO for signaling
implementation 'io.socket:socket.io-client:2.0.1'
// JSON parsing
implementation 'com.google.code.gson:gson:2.10.1'
// Permissions
implementation 'androidx.activity:activity:1.7.2'
implementation 'androidx.fragment:fragment:1.6.1'
// Emoji support
implementation 'com.vanniktech:emoji-java:0.16.0'
implementation 'com.vanniktech:emoji-google:0.16.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

21
android-app/app/proguard-rules.pro vendido Archivo normal
Ver fichero

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

Ver fichero

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<uses-feature android:name="android.hardware.microphone" android:required="false" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ChatRTC"
android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ChatActivity"
android:exported="false"
android:screenOrientation="portrait" />
</application>
</manifest>

Ver fichero

@@ -0,0 +1,229 @@
package com.chatrtc.app;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.chatrtc.app.adapter.ChatAdapter;
import com.chatrtc.app.model.ChatMessage;
import com.chatrtc.app.webrtc.WebRTCManager;
import com.vanniktech.emoji.EmojiManager;
import com.vanniktech.emoji.EmojiPopup;
import com.vanniktech.emoji.google.GoogleEmojiProvider;
import org.webrtc.SurfaceViewRenderer;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class ChatActivity extends AppCompatActivity implements WebRTCManager.WebRTCListener {
private static final String TAG = "ChatActivity";
private RecyclerView rvChat;
private EditText etMessage;
private Button btnSend;
private ImageButton btnEmoji;
private Button btnToggleVideo;
private Button btnToggleAudio;
private Button btnSwitchCamera;
private SurfaceViewRenderer localVideoView;
private SurfaceViewRenderer remoteVideoView;
private ChatAdapter chatAdapter;
private List<ChatMessage> chatMessages;
private String nickname;
private WebRTCManager webRTCManager;
private EmojiPopup emojiPopup;
private boolean isVideoEnabled = true;
private boolean isAudioEnabled = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Initialize emoji support
EmojiManager.install(new GoogleEmojiProvider());
setContentView(R.layout.activity_chat);
// Get nickname from intent
nickname = getIntent().getStringExtra("nickname");
if (TextUtils.isEmpty(nickname)) {
finish();
return;
}
initViews();
setupRecyclerView();
setupClickListeners();
setupEmojiPopup();
// Initialize WebRTC
webRTCManager = new WebRTCManager(this, this);
webRTCManager.initializeWebRTC(localVideoView, remoteVideoView);
webRTCManager.connectToSignalingServer();
// Join chat room
webRTCManager.joinRoom(nickname);
// Add welcome message
addSystemMessage("Welcome to the chat, " + nickname + "!");
}
private void initViews() {
rvChat = findViewById(R.id.rv_chat);
etMessage = findViewById(R.id.et_message);
btnSend = findViewById(R.id.btn_send);
btnEmoji = findViewById(R.id.btn_emoji);
btnToggleVideo = findViewById(R.id.btn_toggle_video);
btnToggleAudio = findViewById(R.id.btn_toggle_audio);
btnSwitchCamera = findViewById(R.id.btn_switch_camera);
localVideoView = findViewById(R.id.local_video_view);
remoteVideoView = findViewById(R.id.remote_video_view);
setTitle("Chat - " + nickname);
}
private void setupRecyclerView() {
chatMessages = new ArrayList<>();
chatAdapter = new ChatAdapter(chatMessages);
rvChat.setLayoutManager(new LinearLayoutManager(this));
rvChat.setAdapter(chatAdapter);
}
private void setupClickListeners() {
btnSend.setOnClickListener(v -> sendMessage());
btnToggleVideo.setOnClickListener(v -> toggleVideo());
btnToggleAudio.setOnClickListener(v -> toggleAudio());
btnSwitchCamera.setOnClickListener(v -> webRTCManager.switchCamera());
etMessage.setOnEditorActionListener((v, actionId, event) -> {
sendMessage();
return true;
});
}
private void setupEmojiPopup() {
emojiPopup = EmojiPopup.Builder.fromRootView(findViewById(android.R.id.content))
.setOnEmojiPopupShownListener(() -> btnEmoji.setImageResource(R.drawable.ic_keyboard))
.setOnEmojiPopupDismissListener(() -> btnEmoji.setImageResource(R.drawable.ic_emoji))
.build(etMessage);
btnEmoji.setOnClickListener(v -> emojiPopup.toggle());
}
private void sendMessage() {
String message = etMessage.getText().toString().trim();
if (TextUtils.isEmpty(message)) {
return;
}
ChatMessage chatMessage = new ChatMessage(nickname, message, new Date(), true);
addMessage(chatMessage);
// Send message through WebRTC data channel
webRTCManager.sendMessage(message);
etMessage.setText("");
}
private void addMessage(ChatMessage message) {
chatMessages.add(message);
chatAdapter.notifyItemInserted(chatMessages.size() - 1);
rvChat.smoothScrollToPosition(chatMessages.size() - 1);
}
private void addSystemMessage(String message) {
ChatMessage systemMessage = new ChatMessage("System", message, new Date(), false);
systemMessage.setSystemMessage(true);
addMessage(systemMessage);
}
private void toggleVideo() {
isVideoEnabled = !isVideoEnabled;
webRTCManager.toggleVideo(isVideoEnabled);
btnToggleVideo.setText(isVideoEnabled ? "Video Off" : "Video On");
btnToggleVideo.setBackgroundColor(getResources().getColor(
isVideoEnabled ? R.color.button_enabled : R.color.button_disabled));
}
private void toggleAudio() {
isAudioEnabled = !isAudioEnabled;
webRTCManager.toggleAudio(isAudioEnabled);
btnToggleAudio.setText(isAudioEnabled ? "Mute" : "Unmute");
btnToggleAudio.setBackgroundColor(getResources().getColor(
isAudioEnabled ? R.color.button_enabled : R.color.button_disabled));
}
// WebRTCListener implementation
@Override
public void onMessageReceived(String senderNickname, String message) {
runOnUiThread(() -> {
ChatMessage chatMessage = new ChatMessage(senderNickname, message, new Date(), false);
addMessage(chatMessage);
});
}
@Override
public void onUserJoined(String userNickname) {
runOnUiThread(() -> addSystemMessage(userNickname + " joined the chat"));
}
@Override
public void onUserLeft(String userNickname) {
runOnUiThread(() -> addSystemMessage(userNickname + " left the chat"));
}
@Override
public void onConnected() {
runOnUiThread(() -> addSystemMessage("Connected to chat"));
}
@Override
public void onDisconnected() {
runOnUiThread(() -> addSystemMessage("Disconnected from chat"));
}
@Override
public void onError(String error) {
runOnUiThread(() -> {
Log.e(TAG, "WebRTC Error: " + error);
Toast.makeText(this, "Error: " + error, Toast.LENGTH_LONG).show();
});
}
@Override
protected void onDestroy() {
super.onDestroy();
if (webRTCManager != null) {
webRTCManager.cleanup();
}
if (emojiPopup != null && emojiPopup.isShowing()) {
emojiPopup.dismiss();
}
}
@Override
public void onBackPressed() {
if (emojiPopup != null && emojiPopup.isShowing()) {
emojiPopup.dismiss();
} else {
super.onBackPressed();
}
}
}

Ver fichero

@@ -0,0 +1,99 @@
package com.chatrtc.app;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.text.TextUtils;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
public class MainActivity extends AppCompatActivity {
private static final int PERMISSIONS_REQUEST_CODE = 100;
private static final String[] REQUIRED_PERMISSIONS = {
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.MODIFY_AUDIO_SETTINGS
};
private EditText etNickname;
private Button btnJoinChat;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
setupClickListeners();
// Check permissions
if (!hasPermissions()) {
requestPermissions();
}
}
private void initViews() {
etNickname = findViewById(R.id.et_nickname);
btnJoinChat = findViewById(R.id.btn_join_chat);
}
private void setupClickListeners() {
btnJoinChat.setOnClickListener(v -> {
String nickname = etNickname.getText().toString().trim();
if (TextUtils.isEmpty(nickname)) {
Toast.makeText(this, "Please enter your nickname", Toast.LENGTH_SHORT).show();
return;
}
if (!hasPermissions()) {
requestPermissions();
return;
}
// Start chat activity
Intent intent = new Intent(this, ChatActivity.class);
intent.putExtra("nickname", nickname);
startActivity(intent);
});
}
private boolean hasPermissions() {
for (String permission : REQUIRED_PERMISSIONS) {
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
private void requestPermissions() {
ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, PERMISSIONS_REQUEST_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSIONS_REQUEST_CODE) {
boolean allGranted = true;
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
allGranted = false;
break;
}
}
if (!allGranted) {
Toast.makeText(this, "Permissions are required for audio/video chat", Toast.LENGTH_LONG).show();
}
}
}
}

Ver fichero

@@ -0,0 +1,127 @@
package com.chatrtc.app.adapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.chatrtc.app.R;
import com.chatrtc.app.model.ChatMessage;
import com.vanniktech.emoji.EmojiTextView;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Locale;
public class ChatAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int VIEW_TYPE_OWN_MESSAGE = 1;
private static final int VIEW_TYPE_OTHER_MESSAGE = 2;
private static final int VIEW_TYPE_SYSTEM_MESSAGE = 3;
private List<ChatMessage> messages;
private SimpleDateFormat timeFormat;
public ChatAdapter(List<ChatMessage> messages) {
this.messages = messages;
this.timeFormat = new SimpleDateFormat("HH:mm", Locale.getDefault());
}
@Override
public int getItemViewType(int position) {
ChatMessage message = messages.get(position);
if (message.isSystemMessage()) {
return VIEW_TYPE_SYSTEM_MESSAGE;
} else if (message.isOwnMessage()) {
return VIEW_TYPE_OWN_MESSAGE;
} else {
return VIEW_TYPE_OTHER_MESSAGE;
}
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
switch (viewType) {
case VIEW_TYPE_OWN_MESSAGE:
return new OwnMessageViewHolder(inflater.inflate(R.layout.item_own_message, parent, false));
case VIEW_TYPE_OTHER_MESSAGE:
return new OtherMessageViewHolder(inflater.inflate(R.layout.item_other_message, parent, false));
case VIEW_TYPE_SYSTEM_MESSAGE:
return new SystemMessageViewHolder(inflater.inflate(R.layout.item_system_message, parent, false));
default:
throw new IllegalArgumentException("Unknown view type: " + viewType);
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
ChatMessage message = messages.get(position);
if (holder instanceof OwnMessageViewHolder) {
((OwnMessageViewHolder) holder).bind(message);
} else if (holder instanceof OtherMessageViewHolder) {
((OtherMessageViewHolder) holder).bind(message);
} else if (holder instanceof SystemMessageViewHolder) {
((SystemMessageViewHolder) holder).bind(message);
}
}
@Override
public int getItemCount() {
return messages.size();
}
class OwnMessageViewHolder extends RecyclerView.ViewHolder {
private EmojiTextView tvMessage;
private TextView tvTime;
public OwnMessageViewHolder(@NonNull View itemView) {
super(itemView);
tvMessage = itemView.findViewById(R.id.tv_message);
tvTime = itemView.findViewById(R.id.tv_time);
}
public void bind(ChatMessage message) {
tvMessage.setText(message.getMessage());
tvTime.setText(timeFormat.format(message.getTimestamp()));
}
}
class OtherMessageViewHolder extends RecyclerView.ViewHolder {
private TextView tvNickname;
private EmojiTextView tvMessage;
private TextView tvTime;
public OtherMessageViewHolder(@NonNull View itemView) {
super(itemView);
tvNickname = itemView.findViewById(R.id.tv_nickname);
tvMessage = itemView.findViewById(R.id.tv_message);
tvTime = itemView.findViewById(R.id.tv_time);
}
public void bind(ChatMessage message) {
tvNickname.setText(message.getSenderNickname());
tvMessage.setText(message.getMessage());
tvTime.setText(timeFormat.format(message.getTimestamp()));
}
}
class SystemMessageViewHolder extends RecyclerView.ViewHolder {
private TextView tvMessage;
public SystemMessageViewHolder(@NonNull View itemView) {
super(itemView);
tvMessage = itemView.findViewById(R.id.tv_system_message);
}
public void bind(ChatMessage message) {
tvMessage.setText(message.getMessage());
}
}
}

Ver fichero

@@ -0,0 +1,59 @@
package com.chatrtc.app.model;
import java.util.Date;
public class ChatMessage {
private String senderNickname;
private String message;
private Date timestamp;
private boolean isOwnMessage;
private boolean isSystemMessage;
public ChatMessage(String senderNickname, String message, Date timestamp, boolean isOwnMessage) {
this.senderNickname = senderNickname;
this.message = message;
this.timestamp = timestamp;
this.isOwnMessage = isOwnMessage;
this.isSystemMessage = false;
}
public String getSenderNickname() {
return senderNickname;
}
public void setSenderNickname(String senderNickname) {
this.senderNickname = senderNickname;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Date getTimestamp() {
return timestamp;
}
public void setTimestamp(Date timestamp) {
this.timestamp = timestamp;
}
public boolean isOwnMessage() {
return isOwnMessage;
}
public void setOwnMessage(boolean ownMessage) {
isOwnMessage = ownMessage;
}
public boolean isSystemMessage() {
return isSystemMessage;
}
public void setSystemMessage(boolean systemMessage) {
isSystemMessage = systemMessage;
}
}

Ver fichero

@@ -0,0 +1,490 @@
package com.chatrtc.app.webrtc;
import android.content.Context;
import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import org.webrtc.AudioSource;
import org.webrtc.AudioTrack;
import org.webrtc.Camera1Enumerator;
import org.webrtc.Camera2Enumerator;
import org.webrtc.CameraEnumerator;
import org.webrtc.CameraVideoCapturer;
import org.webrtc.DataChannel;
import org.webrtc.DefaultVideoDecoderFactory;
import org.webrtc.DefaultVideoEncoderFactory;
import org.webrtc.EglBase;
import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints;
import org.webrtc.MediaStream;
import org.webrtc.PeerConnection;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.SessionDescription;
import org.webrtc.SurfaceTextureHelper;
import org.webrtc.SurfaceViewRenderer;
import org.webrtc.VideoCapturer;
import org.webrtc.VideoRenderer;
import org.webrtc.VideoSource;
import org.webrtc.VideoTrack;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import io.socket.client.IO;
import io.socket.client.Socket;
public class WebRTCManager {
private static final String TAG = "WebRTCManager";
private static final String SIGNALING_SERVER_URL = "https://your-signaling-server.com"; // Replace with your server
public interface WebRTCListener {
void onMessageReceived(String senderNickname, String message);
void onUserJoined(String userNickname);
void onUserLeft(String userNickname);
void onConnected();
void onDisconnected();
void onError(String error);
}
private Context context;
private WebRTCListener listener;
private Socket socket;
private String nickname;
// WebRTC components
private PeerConnectionFactory peerConnectionFactory;
private PeerConnection peerConnection;
private EglBase rootEglBase;
private VideoCapturer videoCapturer;
private VideoSource videoSource;
private AudioSource audioSource;
private VideoTrack localVideoTrack;
private AudioTrack localAudioTrack;
private DataChannel dataChannel;
// UI components
private SurfaceViewRenderer localVideoView;
private SurfaceViewRenderer remoteVideoView;
// State
private boolean isVideoEnabled = true;
private boolean isAudioEnabled = true;
private boolean isFrontCamera = true;
private Gson gson = new Gson();
public WebRTCManager(Context context, WebRTCListener listener) {
this.context = context;
this.listener = listener;
}
public void initializeWebRTC(SurfaceViewRenderer localVideoView, SurfaceViewRenderer remoteVideoView) {
this.localVideoView = localVideoView;
this.remoteVideoView = remoteVideoView;
// Initialize EGL context
rootEglBase = EglBase.create();
// Initialize video views
localVideoView.init(rootEglBase.getEglBaseContext(), null);
remoteVideoView.init(rootEglBase.getEglBaseContext(), null);
// Create peer connection factory
PeerConnectionFactory.initialize(
PeerConnectionFactory.InitializationOptions.builder(context)
.setEnableInternalTracer(true)
.createInitializationOptions()
);
PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
peerConnectionFactory = PeerConnectionFactory.builder()
.setVideoDecoderFactory(new DefaultVideoDecoderFactory(rootEglBase.getEglBaseContext()))
.setVideoEncoderFactory(new DefaultVideoEncoderFactory(rootEglBase.getEglBaseContext(), true, true))
.setOptions(options)
.createPeerConnectionFactory();
// Create local media streams
createLocalMediaStream();
// Create peer connection
createPeerConnection();
}
private void createLocalMediaStream() {
// Create video capturer
videoCapturer = createCameraVideoCapturer();
if (videoCapturer != null) {
SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", rootEglBase.getEglBaseContext());
videoSource = peerConnectionFactory.createVideoSource(videoCapturer.isScreencast());
videoCapturer.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver());
videoCapturer.startCapture(1024, 768, 30);
localVideoTrack = peerConnectionFactory.createVideoTrack("100", videoSource);
localVideoTrack.addSink(localVideoView);
}
// Create audio source
MediaConstraints audioConstraints = new MediaConstraints();
audioSource = peerConnectionFactory.createAudioSource(audioConstraints);
localAudioTrack = peerConnectionFactory.createAudioTrack("101", audioSource);
}
private VideoCapturer createCameraVideoCapturer() {
CameraEnumerator enumerator;
if (Camera2Enumerator.isSupported(context)) {
enumerator = new Camera2Enumerator(context);
} else {
enumerator = new Camera1Enumerator(true);
}
final String[] deviceNames = enumerator.getDeviceNames();
// Try front camera first
for (String deviceName : deviceNames) {
if (enumerator.isFrontFacing(deviceName)) {
VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
if (videoCapturer != null) {
return videoCapturer;
}
}
}
// Try back camera
for (String deviceName : deviceNames) {
if (!enumerator.isFrontFacing(deviceName)) {
VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
if (videoCapturer != null) {
return videoCapturer;
}
}
}
return null;
}
private void createPeerConnection() {
ArrayList<PeerConnection.IceServer> iceServers = new ArrayList<>();
iceServers.add(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer());
PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED;
rtcConfig.bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE;
rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE;
rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
rtcConfig.keyType = PeerConnection.KeyType.ECDSA;
peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, new PeerConnectionObserver());
// Create data channel for text messages
DataChannel.Init dataChannelInit = new DataChannel.Init();
dataChannel = peerConnection.createDataChannel("messages", dataChannelInit);
dataChannel.registerObserver(new DataChannelObserver());
// Add local tracks
if (localVideoTrack != null) {
peerConnection.addTrack(localVideoTrack, Arrays.asList("stream"));
}
if (localAudioTrack != null) {
peerConnection.addTrack(localAudioTrack, Arrays.asList("stream"));
}
}
public void connectToSignalingServer() {
try {
socket = IO.socket(SIGNALING_SERVER_URL);
socket.on(Socket.EVENT_CONNECT, args -> {
Log.d(TAG, "Connected to signaling server");
listener.onConnected();
});
socket.on(Socket.EVENT_DISCONNECT, args -> {
Log.d(TAG, "Disconnected from signaling server");
listener.onDisconnected();
});
socket.on("user-joined", args -> {
String userNickname = (String) args[0];
listener.onUserJoined(userNickname);
});
socket.on("user-left", args -> {
String userNickname = (String) args[0];
listener.onUserLeft(userNickname);
});
socket.on("offer", args -> {
JsonObject data = gson.fromJson(args[0].toString(), JsonObject.class);
handleOffer(data);
});
socket.on("answer", args -> {
JsonObject data = gson.fromJson(args[0].toString(), JsonObject.class);
handleAnswer(data);
});
socket.on("ice-candidate", args -> {
JsonObject data = gson.fromJson(args[0].toString(), JsonObject.class);
handleIceCandidate(data);
});
socket.connect();
} catch (Exception e) {
Log.e(TAG, "Error connecting to signaling server", e);
listener.onError("Failed to connect to signaling server");
}
}
public void joinRoom(String nickname) {
this.nickname = nickname;
if (socket != null && socket.connected()) {
socket.emit("join-room", nickname);
}
}
public void sendMessage(String message) {
if (dataChannel != null && dataChannel.state() == DataChannel.State.OPEN) {
JsonObject messageObj = new JsonObject();
messageObj.addProperty("type", "message");
messageObj.addProperty("nickname", nickname);
messageObj.addProperty("message", message);
String jsonMessage = gson.toJson(messageObj);
ByteBuffer buffer = ByteBuffer.wrap(jsonMessage.getBytes(StandardCharsets.UTF_8));
DataChannel.Buffer dataBuffer = new DataChannel.Buffer(buffer, false);
dataChannel.send(dataBuffer);
}
}
public void toggleVideo(boolean enabled) {
isVideoEnabled = enabled;
if (localVideoTrack != null) {
localVideoTrack.setEnabled(enabled);
}
}
public void toggleAudio(boolean enabled) {
isAudioEnabled = enabled;
if (localAudioTrack != null) {
localAudioTrack.setEnabled(enabled);
}
}
public void switchCamera() {
if (videoCapturer != null && videoCapturer instanceof CameraVideoCapturer) {
CameraVideoCapturer cameraVideoCapturer = (CameraVideoCapturer) videoCapturer;
cameraVideoCapturer.switchCamera(null);
isFrontCamera = !isFrontCamera;
}
}
private void handleOffer(JsonObject data) {
SessionDescription offer = new SessionDescription(
SessionDescription.Type.OFFER,
data.get("sdp").getAsString()
);
peerConnection.setRemoteDescription(new SdpObserver(), offer);
// Create answer
MediaConstraints constraints = new MediaConstraints();
peerConnection.createAnswer(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
peerConnection.setLocalDescription(new SdpObserver(), sessionDescription);
JsonObject answerData = new JsonObject();
answerData.addProperty("type", sessionDescription.type.canonicalForm());
answerData.addProperty("sdp", sessionDescription.description);
socket.emit("answer", answerData);
}
}, constraints);
}
private void handleAnswer(JsonObject data) {
SessionDescription answer = new SessionDescription(
SessionDescription.Type.ANSWER,
data.get("sdp").getAsString()
);
peerConnection.setRemoteDescription(new SdpObserver(), answer);
}
private void handleIceCandidate(JsonObject data) {
IceCandidate iceCandidate = new IceCandidate(
data.get("sdpMid").getAsString(),
data.get("sdpMLineIndex").getAsInt(),
data.get("candidate").getAsString()
);
peerConnection.addIceCandidate(iceCandidate);
}
public void cleanup() {
if (localVideoTrack != null) {
localVideoTrack.dispose();
}
if (localAudioTrack != null) {
localAudioTrack.dispose();
}
if (videoCapturer != null) {
try {
videoCapturer.stopCapture();
} catch (InterruptedException e) {
e.printStackTrace();
}
videoCapturer.dispose();
}
if (videoSource != null) {
videoSource.dispose();
}
if (audioSource != null) {
audioSource.dispose();
}
if (peerConnection != null) {
peerConnection.close();
}
if (peerConnectionFactory != null) {
peerConnectionFactory.dispose();
}
if (socket != null) {
socket.disconnect();
}
if (rootEglBase != null) {
rootEglBase.release();
}
}
// Observer classes
private class PeerConnectionObserver implements PeerConnection.Observer {
@Override
public void onSignalingChange(PeerConnection.SignalingState signalingState) {
Log.d(TAG, "onSignalingChange: " + signalingState);
}
@Override
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
Log.d(TAG, "onIceConnectionChange: " + iceConnectionState);
}
@Override
public void onIceConnectionReceivingChange(boolean b) {
Log.d(TAG, "onIceConnectionReceivingChange: " + b);
}
@Override
public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
Log.d(TAG, "onIceGatheringChange: " + iceGatheringState);
}
@Override
public void onIceCandidate(IceCandidate iceCandidate) {
Log.d(TAG, "onIceCandidate: " + iceCandidate);
JsonObject candidateData = new JsonObject();
candidateData.addProperty("candidate", iceCandidate.sdp);
candidateData.addProperty("sdpMid", iceCandidate.sdpMid);
candidateData.addProperty("sdpMLineIndex", iceCandidate.sdpMLineIndex);
socket.emit("ice-candidate", candidateData);
}
@Override
public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
Log.d(TAG, "onIceCandidatesRemoved");
}
@Override
public void onAddStream(MediaStream mediaStream) {
Log.d(TAG, "onAddStream: " + mediaStream.getId());
if (mediaStream.videoTracks.size() > 0) {
VideoTrack remoteVideoTrack = mediaStream.videoTracks.get(0);
remoteVideoTrack.addSink(remoteVideoView);
}
}
@Override
public void onRemoveStream(MediaStream mediaStream) {
Log.d(TAG, "onRemoveStream: " + mediaStream.getId());
}
@Override
public void onDataChannel(DataChannel dataChannel) {
Log.d(TAG, "onDataChannel: " + dataChannel.label());
dataChannel.registerObserver(new DataChannelObserver());
}
@Override
public void onRenegotiationNeeded() {
Log.d(TAG, "onRenegotiationNeeded");
}
@Override
public void onAddTrack(org.webrtc.RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
Log.d(TAG, "onAddTrack");
}
}
private class DataChannelObserver implements DataChannel.Observer {
@Override
public void onBufferedAmountChange(long l) {
Log.d(TAG, "Data channel buffered amount changed: " + l);
}
@Override
public void onStateChange() {
Log.d(TAG, "Data channel state changed: " + dataChannel.state());
}
@Override
public void onMessage(DataChannel.Buffer buffer) {
ByteBuffer data = buffer.data;
byte[] bytes = new byte[data.remaining()];
data.get(bytes);
String message = new String(bytes, StandardCharsets.UTF_8);
try {
JsonObject messageObj = gson.fromJson(message, JsonObject.class);
String type = messageObj.get("type").getAsString();
if ("message".equals(type)) {
String senderNickname = messageObj.get("nickname").getAsString();
String messageText = messageObj.get("message").getAsString();
listener.onMessageReceived(senderNickname, messageText);
}
} catch (Exception e) {
Log.e(TAG, "Error parsing received message", e);
}
}
}
private static class SdpObserver implements org.webrtc.SdpObserver {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
Log.d(TAG, "SDP create success");
}
@Override
public void onSetSuccess() {
Log.d(TAG, "SDP set success");
}
@Override
public void onCreateFailure(String s) {
Log.e(TAG, "SDP create failure: " + s);
}
@Override
public void onSetFailure(String s) {
Log.e(TAG, "SDP set failure: " + s);
}
}
}

Ver fichero

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/button_background" />
<stroke android:width="1dp" android:color="@color/button_border" />
</shape>

Ver fichero

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/primary_color" />
</shape>

Ver fichero

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#80000000" />
<corners android:radius="24dp" />
</shape>

Ver fichero

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/icon_color">
<path
android:fillColor="@android:color/white"
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,14L6,14v-2h12v2zM18,11L6,11L6,9h12v2zM18,8L6,8L6,6h12v2z"/>
</vector>

Ver fichero

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/icon_color">
<path
android:fillColor="@android:color/white"
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM8.5,8C9.33,8 10,8.67 10,9.5S9.33,11 8.5,11S7,10.33 7,9.5S7.67,8 8.5,8zM15.5,8c0.83,0 1.5,0.67 1.5,1.5S16.33,11 15.5,11S14,10.33 14,9.5S14.67,8 15.5,8zM12,17.5c-2.33,0 -4.31,-1.46 -5.11,-3.5h10.22C16.31,16.04 14.33,17.5 12,17.5z"/>
</vector>

Ver fichero

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/icon_color">
<path
android:fillColor="@android:color/white"
android:pathData="M20,5L4,5c-1.1,0 -1.99,0.9 -1.99,2L2,17c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,7c0,-1.1 -0.9,-2 -2,-2zM20,9h-3.5v3.5h2.5c0.83,0 1.5,-0.67 1.5,-1.5L20,9zM20,17h-7v-7h7v7zM4,9h7v7L4,16L4,9zM4,7h16v1L4,8L4,7z"/>
</vector>

Ver fichero

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path android:fillColor="#00000000"
android:pathData="M9,0L9,108L0,108L0,0L9,0Z"
android:strokeWidth="0"
android:strokeMiterLimit="4"
android:strokeLineCap="butt" />
</vector>

Ver fichero

@@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="2.61"
android:scaleY="2.61"
android:translateX="22.68"
android:translateY="22.68">
<path
android:fillColor="#FFFFFF"
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,14L6,14v-2h12v2zM18,11L6,11L6,9h12v2zM18,8L6,8L6,6h12v2z"/>
</group>
</vector>

Ver fichero

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/input_field_background" />
<corners android:radius="24dp" />
<stroke android:width="1dp" android:color="@color/input_border" />
</shape>

Ver fichero

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/message_background" />
<corners android:radius="18dp" />
<stroke android:width="1dp" android:color="@color/message_border" />
</shape>

Ver fichero

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/primary_color" />
<corners android:radius="18dp" />
</shape>

Ver fichero

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/system_background" />
<corners android:radius="12dp" />
</shape>

Ver fichero

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="8dp" />
<stroke android:width="2dp" android:color="@color/white" />
</shape>

Ver fichero

@@ -0,0 +1,130 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/background_color">
<!-- Video Views Container -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@color/video_background">
<!-- Remote Video View (Full Screen) -->
<org.webrtc.SurfaceViewRenderer
android:id="@+id/remote_video_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- Local Video View (Picture in Picture) -->
<org.webrtc.SurfaceViewRenderer
android:id="@+id/local_video_view"
android:layout_width="120dp"
android:layout_height="160dp"
android:layout_gravity="top|end"
android:layout_margin="16dp"
android:background="@drawable/video_border" />
<!-- Video Controls -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:layout_marginBottom="16dp"
android:orientation="horizontal"
android:background="@drawable/control_background"
android:padding="8dp">
<Button
android:id="@+id/btn_toggle_video"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_margin="4dp"
android:background="@drawable/btn_round_background"
android:text="📹"
android:textSize="18sp" />
<Button
android:id="@+id/btn_toggle_audio"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_margin="4dp"
android:background="@drawable/btn_round_background"
android:text="🎤"
android:textSize="18sp" />
<Button
android:id="@+id/btn_switch_camera"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_margin="4dp"
android:background="@drawable/btn_round_background"
android:text="🔄"
android:textSize="18sp" />
</LinearLayout>
</FrameLayout>
<!-- Chat Section -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="300dp"
android:orientation="vertical"
android:background="@color/chat_background">
<!-- Chat Messages -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_chat"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="8dp"
android:clipToPadding="false" />
<!-- Message Input -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
android:background="@color/input_background"
android:elevation="4dp">
<ImageButton
android:id="@+id/btn_emoji"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="8dp"
android:background="@drawable/btn_round_background"
android:src="@drawable/ic_emoji"
android:contentDescription="@string/emoji_button" />
<EditText
android:id="@+id/et_message"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:background="@drawable/message_input_background"
android:hint="@string/message_hint"
android:maxLines="3"
android:padding="12dp"
android:textSize="16sp" />
<Button
android:id="@+id/btn_send"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:background="@drawable/btn_send_background"
android:text="➤"
android:textColor="@color/white"
android:textSize="18sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

Ver fichero

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp"
android:gravity="center"
android:background="@color/background_color">
<ImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:src="@drawable/ic_chat"
android:layout_marginBottom="32dp"
android:contentDescription="@string/app_name" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/welcome_title"
android:textSize="28sp"
android:textStyle="bold"
android:textColor="@color/primary_text_color"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/welcome_subtitle"
android:textSize="16sp"
android:textColor="@color/secondary_text_color"
android:layout_marginBottom="32dp"
android:gravity="center" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:hint="@string/nickname_hint"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_nickname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:maxLines="1"
android:imeOptions="actionDone" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btn_join_chat"
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="@string/join_chat"
android:textSize="16sp"
android:textStyle="bold"
style="@style/Widget.MaterialComponents.Button" />
</LinearLayout>

Ver fichero

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
android:gravity="start">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="48dp"
android:orientation="vertical"
android:background="@drawable/other_message_background"
android:padding="12dp">
<TextView
android:id="@+id/tv_nickname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/primary_color"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginBottom="2dp" />
<com.vanniktech.emoji.EmojiTextView
android:id="@+id/tv_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/primary_text_color"
android:textSize="16sp"
android:lineSpacingExtra="2dp" />
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="4dp"
android:textColor="@color/secondary_text_color"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>

Ver fichero

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
android:gravity="end">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="48dp"
android:orientation="vertical"
android:background="@drawable/own_message_background"
android:padding="12dp">
<com.vanniktech.emoji.EmojiTextView
android:id="@+id/tv_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="16sp"
android:lineSpacingExtra="2dp" />
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="4dp"
android:textColor="@color/message_time_color"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>

Ver fichero

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center">
<TextView
android:id="@+id/tv_system_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/system_message_background"
android:textColor="@color/system_text_color"
android:textSize="14sp"
android:textStyle="italic"
android:padding="8dp" />
</LinearLayout>

Ver fichero

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Ver fichero

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Ver fichero

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primary_color">#2196F3</color>
<color name="primary_color_dark">#1976D2</color>
<color name="accent_color">#FF4081</color>
<color name="background_color">#FAFAFA</color>
<color name="chat_background">#FFFFFF</color>
<color name="video_background">#000000</color>
<color name="input_background">#FFFFFF</color>
<color name="primary_text_color">#212121</color>
<color name="secondary_text_color">#757575</color>
<color name="system_text_color">#9E9E9E</color>
<color name="message_background">#F5F5F5</color>
<color name="message_border">#E0E0E0</color>
<color name="message_time_color">#B0BEC5</color>
<color name="system_background">#E8F5E8</color>
<color name="input_field_background">#FFFFFF</color>
<color name="input_border">#E0E0E0</color>
<color name="button_background">#FFFFFF</color>
<color name="button_border">#E0E0E0</color>
<color name="button_enabled">#4CAF50</color>
<color name="button_disabled">#F44336</color>
<color name="icon_color">#616161</color>
<color name="white">#FFFFFF</color>
<color name="black">#000000</color>
</resources>

Ver fichero

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">ChatRTC</string>
<!-- Main Activity -->
<string name="welcome_title">Welcome to ChatRTC</string>
<string name="welcome_subtitle">Connect with others using audio and video chat</string>
<string name="nickname_hint">Enter your nickname</string>
<string name="join_chat">Join Chat</string>
<!-- Chat Activity -->
<string name="message_hint">Type your message...</string>
<string name="emoji_button">Emoji</string>
<!-- Permissions -->
<string name="permission_camera">Camera permission is required for video chat</string>
<string name="permission_microphone">Microphone permission is required for audio chat</string>
<!-- Errors -->
<string name="error_nickname_empty">Please enter your nickname</string>
<string name="error_connection">Connection failed. Please try again.</string>
<string name="error_webrtc">WebRTC error occurred</string>
</resources>

Ver fichero

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme -->
<style name="Theme.ChatRTC" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color -->
<item name="colorPrimary">@color/primary_color</item>
<item name="colorPrimaryVariant">@color/primary_color_dark</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color -->
<item name="colorSecondary">@color/accent_color</item>
<item name="colorSecondaryVariant">@color/accent_color</item>
<item name="colorOnSecondary">@color/white</item>
<!-- Status bar color -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here -->
<item name="android:windowBackground">@color/background_color</item>
</style>
</resources>

Ver fichero

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<exclude domain="sharedpref" path="device_prefs.xml"/>
</full-backup-content>

Ver fichero

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<exclude domain="root" />
<exclude domain="device_transfer" />
</cloud-backup>
<device-transfer>
<exclude domain="root" />
</device-transfer>
</data-extraction-rules>

Ver fichero

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">10.0.2.2</domain>
<domain includeSubdomains="true">192.168.1.1</domain>
</domain-config>
</network-security-config>

24
android-app/build.gradle Archivo normal
Ver fichero

@@ -0,0 +1,24 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.8.20"
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:8.0.2"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

28
android-app/gradle.properties Archivo normal
Ver fichero

@@ -0,0 +1,28 @@
gradle.properties
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official

Ver fichero

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

2
android-app/settings.gradle Archivo normal
Ver fichero

@@ -0,0 +1,2 @@
include ':app'
rootProject.name = "ChatRTC"

22
android-app/setup-icons.sh Archivo ejecutable
Ver fichero

@@ -0,0 +1,22 @@
#!/bin/bash
# Create basic launcher icons for the app
# This script creates placeholder PNG files for the app icons
APP_DIR="/home/ale/projects/android/chatrtc/app/src/main/res"
# Create mipmap directories
mkdir -p "$APP_DIR/mipmap-hdpi"
mkdir -p "$APP_DIR/mipmap-mdpi"
mkdir -p "$APP_DIR/mipmap-xhdpi"
mkdir -p "$APP_DIR/mipmap-xxhdpi"
mkdir -p "$APP_DIR/mipmap-xxxhdpi"
echo "Mipmap directories created. You can add your custom app icons to these directories:"
echo "- mipmap-mdpi/ic_launcher.png (48x48px)"
echo "- mipmap-hdpi/ic_launcher.png (72x72px)"
echo "- mipmap-xhdpi/ic_launcher.png (96x96px)"
echo "- mipmap-xxhdpi/ic_launcher.png (144x144px)"
echo "- mipmap-xxxhdpi/ic_launcher.png (192x192px)"
echo ""
echo "For now, the app will use the vector drawable icons defined in the XML files."

Ver fichero

@@ -0,0 +1,21 @@
{
"name": "chatrtc-signaling-server",
"version": "1.0.0",
"description": "WebRTC signaling server for ChatRTC app",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2",
"socket.io": "^4.7.2",
"cors": "^2.8.5"
},
"devDependencies": {
"nodemon": "^3.0.1"
},
"keywords": ["webrtc", "signaling", "socket.io", "chat"],
"author": "ChatRTC Team",
"license": "MIT"
}

Ver fichero

@@ -0,0 +1,61 @@
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const cors = require('cors');
const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
cors: {
origin: "*",
methods: ["GET", "POST"]
}
});
app.use(cors());
app.use(express.static('public'));
const users = new Map();
io.on('connection', (socket) => {
console.log('User connected:', socket.id);
socket.on('join-room', (nickname) => {
socket.nickname = nickname;
users.set(socket.id, nickname);
// Notify others about new user
socket.broadcast.emit('user-joined', nickname);
console.log(`${nickname} joined the room`);
});
socket.on('offer', (data) => {
console.log('Offer received from:', socket.nickname);
socket.broadcast.emit('offer', data);
});
socket.on('answer', (data) => {
console.log('Answer received from:', socket.nickname);
socket.broadcast.emit('answer', data);
});
socket.on('ice-candidate', (data) => {
console.log('ICE candidate received from:', socket.nickname);
socket.broadcast.emit('ice-candidate', data);
});
socket.on('disconnect', () => {
const nickname = users.get(socket.id);
if (nickname) {
users.delete(socket.id);
socket.broadcast.emit('user-left', nickname);
console.log(`${nickname} left the room`);
}
});
});
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Signaling server running on port ${PORT}`);
});