Správa záznamu hovorů v klientovi
Důležité
Funkce popsané v tomto článku jsou aktuálně ve verzi Public Preview. Tato verze Preview je poskytována bez smlouvy o úrovni služeb a nedoporučujeme ji pro produkční úlohy. Některé funkce se nemusí podporovat nebo mohou mít omezené možnosti. Další informace najdete v dodatečných podmínkách použití pro verze Preview v Microsoft Azure.
Záznam hovorů umožňuje uživatelům zaznamenávat hovory, které dělají pomocí služeb Azure Communication Services. V tomto článku se dozvíte, jak spravovat nahrávání na straně klienta. Než začnete, musíte nastavit záznam na straně serveru.
Požadavky
- Účet Azure s aktivním předplatným. Vytvoření účtu zdarma
- Nasazený prostředek komunikační služby. Vytvořte prostředek komunikační služby.
- Přístupový token uživatele pro povolení volajícího klienta. Další informace najdete v tématu Vytváření a správa přístupových tokenů.
- Volitelné: Dokončení rychlého startu pro přidání hlasového volání do aplikace
Technická podpora
Následující tabulky definují podporu nahrávání ve službě Azure Communication Services.
Identity a typy volání
Následující tabulky ukazují podporu záznamu pro konkrétní typ a identitu volání.
Identity | Schůzka v Teams | Místnost | 1:1 hovor | Skupinový hovor | 1:1 Zprostředkovatele spolupráce v Teams | Skupinový interopový hovor v Teams |
---|---|---|---|---|---|---|
Uživatel komunikačních služeb | ✔️ [1] | ✔️ [2] | ✔️ [2] | ✔️ [2] | ✔️ [1] | ✔️ [1][2] |
Uživatel Microsoftu 365 | ✔️ [1] | ✔️ [2] | ✔️ [1] | ✔️ [1][2] |
[1] Tyto typy hovorů podporují nahrávání v cloudu a dodržování předpisů v Teams.
[2] Tyto typy volání podporují záznam služby Azure Communication Services.
Operace
Následující tabulky ukazují podporu jednotlivých rozhraní API při volání sady SDK k jednotlivým typům identit.
Operace | Uživatel komunikačních služeb | Uživatel Microsoftu 365 |
---|---|---|
Oznámení o zahájení nebo zastavení záznamu | ✔️ | ✔️ |
Získání stavu záznamu | ✔️ | ✔️ |
Získání oznámení o dostupnosti záznamu | ✔️ [1] | ✔️ [1] |
Informace o tom, jestli je vyžadován explicitní souhlas | ✔️ [2] | ✔️ [2] |
Udělení výslovného souhlasu pro zaznamenání | ✔️ [2] | ✔️ [2] |
[1] Uživatel není upozorněn na dostupnost záznamu. Záznam cloudu Teams můžete získat prostřednictvím rozhraní Microsoft Graph API. Pokud je záznam dostupný, můžete se přihlásit k odběru oznámení ve službě Azure Communication Services.
[2] Tato funkce je k dispozici pouze ve schůzkách Teams a skupinových voláních interoperability Teams.
Sady SDK
Následující tabulky ukazují podporu nahrávání v jednotlivých sadách SDK služby Azure Communication Services.
Platformy | Web | Webové uživatelské rozhraní | iOS | Uživatelské rozhraní iOS | Android | Uživatelské rozhraní Androidu | Windows |
---|---|---|---|---|---|---|---|
Je podporováno. | ✔️ | ✔️ [1] | ✔️ [1] | ✔️ [1] | ✔️ [1] | ✔️ [1] | ✔️ [1] |
[1] Tyto sady SDK nepodporují explicitní souhlas.
Nainstalujte sadu SDK .
npm install
Pomocí příkazu nainstalujte sadu AZURE Communication Services Common and Calling SDK pro JavaScript:
npm install @azure/communication-common --save
npm install @azure/communication-calling --save
Inicializace požadovaných objektů
Pro CallClient
většinu operací volání se vyžaduje instance. Když vytvoříte novou CallClient
instanci, můžete ji nakonfigurovat s vlastními možnostmi, jako je Logger
instance.
CallClient
S instancí můžete vytvořit CallAgent
instanci voláním createCallAgent
. Tato metoda asynchronně vrátí CallAgent
objekt instance.
Metoda createCallAgent
se používá CommunicationTokenCredential
jako argument. Přijímá přístupový token uživatele.
K přístupu deviceManager
můžete použít metodu getDeviceManager
v CallClient
instanci .
const { CallClient } = require('@azure/communication-calling');
const { AzureCommunicationTokenCredential} = require('@azure/communication-common');
const { AzureLogger, setLogLevel } = require("@azure/logger");
// Set the logger's log level
setLogLevel('verbose');
// Redirect log output to console, file, buffer, REST API, or whatever location you want
AzureLogger.log = (...args) => {
console.log(...args); // Redirect log output to console
};
const userToken = '<USER_TOKEN>';
callClient = new CallClient(options);
const tokenCredential = new AzureCommunicationTokenCredential(userToken);
const callAgent = await callClient.createCallAgent(tokenCredential, {displayName: 'optional Azure Communication Services user name'});
const deviceManager = await callClient.getDeviceManager()
Jak nejlépe spravovat připojení sady SDK k infrastruktuře Microsoftu
Tato Call Agent
instance vám pomůže spravovat hovory (pro připojení nebo zahájení hovorů). Aby bylo možné pracovat s vaší sadou SDK volání, musí se připojit k infrastruktuře Microsoftu, aby bylo možné dostávat oznámení o příchozích hovorech a koordinovat další podrobnosti volání. Máte Call Agent
dva možné stavy:
Připojeno – Call Agent
Hodnota connectionStatue znamená, Connected
že klientská sada SDK je připojená a dokáže přijímat oznámení z infrastruktury Microsoftu.
Odpojeno – Call Agent
Hodnota Disconnected
connectionStatue stavů existuje problém, který brání sadě SDK, aby se správně připojil. Call Agent
by se mělo znovu vytvořit.
invalidToken
: Pokud vypršela platnost tokenu nebo je neplatnáCall Agent
instance, odpojí se s touto chybou.connectionIssue
: Pokud došlo k problému s klientem, který se připojuje k infrascture Microsoftu, po mnoha opakovaných pokusechCall Agent
connectionIssue
se zobrazí chyba.
Zkontrolujte, jestli je vaše místní Call Agent
infrastruktura připojená k infrastruktuře Microsoftu, a to kontrolou aktuální hodnoty connectionState
vlastnosti. Během aktivního volání můžete naslouchat connectionStateChanged
události a zjistit, jestli Call Agent
se změny ze stavu Připojeno k Odpojeno .
const connectionState = callAgentInstance.connectionState;
console.log(connectionState); // it may return either of 'Connected' | 'Disconnected'
const connectionStateCallback = (args) => {
console.log(args); // it will return an object with oldState and newState, each of having a value of either of 'Connected' | 'Disconnected'
// it will also return reason, either of 'invalidToken' | 'connectionIssue'
}
callAgentInstance.on('connectionStateChanged', connectionStateCallback);
Poznámka:
Toto rozhraní API je poskytováno jako verze Preview pro vývojáře a může se měnit na základě zpětné vazby, kterou obdržíme. Toto rozhraní API nepoužívejte v produkčním prostředí. Pokud chcete toto rozhraní API použít, použijte beta verzi webové sady SDK pro volání služeb Azure Communication Services.
Záznam cloudu
Záznam volání je rozšířená funkce základního rozhraní API pro volání. Nejdřív je potřeba importovat funkce volání ze sady SDK pro volání:
import { Features} from "@azure/communication-calling";
Pak můžete z instance volání získat objekt rozhraní API pro funkce záznamu:
const callRecordingApi = call.feature(Features.Recording);
Chcete-li zkontrolovat, zda je hovor zaznamenán, zkontrolujte isRecordingActive
vlastnost callRecordingApi
. Boolean
Vrátí .
const isRecordingActive = callRecordingApi.isRecordingActive;
Můžete se také přihlásit k odběru změn nahrávání:
const isRecordingActiveChangedHandler = () => {
console.log(callRecordingApi.isRecordingActive);
};
callRecordingApi.on('isRecordingActiveChanged', isRecordingActiveChangedHandler);
Seznam nahrávek můžete získat pomocí recordings
vlastnosti callRecordingApi
. Vrátí RecordingInfo[]
hodnotu , která má aktuální stav cloudového záznamu.
const recordings = callRecordingApi.recordings;
recordings.forEach(r => {
console.log("State: ${r.state}");
Můžete se také přihlásit k recordingsUpdated
odběru a získat kolekci aktualizovaných nahrávek. Tato událost se aktivuje při každé aktualizaci záznamu.
const cloudRecordingsUpdatedHandler = (args: { added: SDK.RecordingInfo[], removed: SDK.RecordingInfo[]}) => {
console.log('Recording started by: ');
args.added?.forEach(a => {
console.log('State: ${a.state}');
});
console.log('Recording stopped by: ');
args.removed?.forEach(r => {
console.log('State: ${r.state}');
});
};
callRecordingApi.on('recordingsUpdated', cloudRecordingsUpdatedHandler );
Explicitní souhlas
Pokud je vaše schůzka nebo hovor v Teams nakonfigurovaný tak, aby vyžadoval explicitní souhlas se záznamem a přepisem, musíte shromáždit souhlas od všech účastníků hovoru, než je budete moct nahrát. Souhlas můžete poskytnout proaktivně při připojování ke schůzce nebo reaktivnímu přijetí záznamu. Dokud explicitní souhlas neudělíte, během nahrávání se zakáže zvuk, video a sdílení obrazovky účastníků.
Můžete zkontrolovat, jestli záznam schůzky vyžaduje explicitní souhlas podle vlastnosti isTeamsConsentRequired
. Pokud je hodnota nastavena na true
, pak explicitní souhlas je vyžadován pro call
.
const isConsentRequired = callRecordingApi.isTeamsConsentRequired;
Pokud jste již získali souhlas uživatele s nahráváním, můžete volat grantTeamsConsent()
metodu označující explicitní souhlas se službou. Tento souhlas platí jenom pro jednu call
relaci a uživatelé musí znovu poskytnout souhlas, pokud se znovu připojí ke schůzce.
callRecordingApi.grantTeamsConsent();
Pokusy o povolení sdílení zvuku, videa nebo obrazovky selžou, když je nahrávání aktivní, vyžaduje se explicitní souhlas, ale ještě není zadaný. Tuto situaci můžete rozpoznat tak, že zkontrolujete vlastnost třídy reason
ParticipantCapabilities
schopností unmuteMic
turnVideoOn
a shareScreen
. Tyto funkce najdete v této funkci call.feature(Features.Capabilities)
. Tyto funkce by vrátily důvod ExplicitConsentRequired
, protože uživatelé potřebují poskytnout explicitní souhlas.
Nainstalujte sadu SDK .
Vyhledejte soubor na úrovni build.gradle
projektu a přidejte mavenCentral()
ho do seznamu úložišť v části buildscript
a allprojects
:
buildscript {
repositories {
...
mavenCentral()
...
}
}
allprojects {
repositories {
...
mavenCentral()
...
}
}
Potom do souboru na úrovni build.gradle
modulu přidejte do oddílu dependencies
následující řádky:
dependencies {
...
implementation 'com.azure.android:azure-communication-calling:1.0.0'
...
}
Inicializace požadovaných objektů
Pokud chcete vytvořit CallAgent
instanci, musíte metodu createCallAgent
CallClient
volat v instanci. Toto volání asynchronně vrátí CallAgent
objekt instance.
Metoda createCallAgent
přebírá CommunicationUserCredential
jako argument, který zapouzdřuje přístupový token.
Pokud chcete získat přístup DeviceManager
, musíte nejprve vytvořit callAgent
instanci. Pak můžete použít metodu CallClient.getDeviceManager
získat DeviceManager
.
String userToken = '<user token>';
CallClient callClient = new CallClient();
CommunicationTokenCredential tokenCredential = new CommunicationTokenCredential(userToken);
android.content.Context appContext = this.getApplicationContext(); // From within an activity, for instance
CallAgent callAgent = callClient.createCallAgent(appContext, tokenCredential).get();
DeviceManager deviceManager = callClient.getDeviceManager(appContext).get();
Pokud chcete nastavit zobrazovaný název volajícího, použijte tuto alternativní metodu:
String userToken = '<user token>';
CallClient callClient = new CallClient();
CommunicationTokenCredential tokenCredential = new CommunicationTokenCredential(userToken);
android.content.Context appContext = this.getApplicationContext(); // From within an activity, for instance
CallAgentOptions callAgentOptions = new CallAgentOptions();
callAgentOptions.setDisplayName("Alice Bob");
DeviceManager deviceManager = callClient.getDeviceManager(appContext).get();
CallAgent callAgent = callClient.createCallAgent(appContext, tokenCredential, callAgentOptions).get();
Záznam hovorů
Poznámka:
Toto rozhraní API je poskytováno jako verze Preview pro vývojáře a může se měnit na základě zpětné vazby, kterou obdržíme. Toto rozhraní API nepoužívejte v produkčním prostředí. Pokud chcete toto rozhraní API použít, použijte beta verzi sady Azure Communication Services, která volá sadu Android SDK.
Záznam volání je rozšířená funkce základního Call
objektu.
Upozorňující
Až do verze 1.1.0 a beta verze 1.1.0-beta.1 sady Azure Communication Services Volající sadu Android SDK isRecordingActive
a addOnIsRecordingActiveChangedListener
byla součástí objektu Call
. U nových beta verzí se tato rozhraní API přesunula jako rozšířená funkce Call
.
Nejprve potřebujete získat objekt funkce záznamu:
RecordingCallFeature callRecordingFeature = call.feature(Features.RECORDING);
Potom zkontrolujte, jestli se hovor nahrává, zkontrolujte isRecordingActive
vlastnost callRecordingFeature
. boolean
Vrátí .
boolean isRecordingActive = callRecordingFeature.isRecordingActive();
Můžete se také přihlásit k odběru změn nahrávání:
private void handleCallOnIsRecordingChanged(PropertyChangedEvent args) {
boolean isRecordingActive = callRecordingFeature.isRecordingActive();
}
callRecordingFeature.addOnIsRecordingActiveChangedListener(handleCallOnIsRecordingChanged);
Pokud chcete začít s nahráváním z aplikace, nejprve postupujte podle přehledu záznamu hovoru a nastavte záznam hovoru.
Po nastavení záznamu hovoru na serveru musíte z aplikace pro Android získat ServerCallId
hodnotu z hovoru a pak ji odeslat na server, aby se spustil proces nahrávání. Hodnotu najdete ServerCallId
pomocí getServerCallId()
CallInfo
třídy. Třídu v objektu třídy můžete najít CallInfo
pomocí getInfo()
.
try {
String serverCallId = call.getInfo().getServerCallId().get();
// Send serverCallId to your recording server to start the call recording.
} catch (ExecutionException | InterruptedException e) {
} catch (UnsupportedOperationException unsupportedOperationException) {
}
Když začnete zaznamenávat ze serveru, událost handleCallOnIsRecordingChanged
se aktivuje a hodnota callRecordingFeature.isRecordingActive()
je true
.
Stejně jako spuštění záznamu hovoru, pokud chcete záznam hovoru zastavit, musíte ho dostat ServerCallId
a odeslat na server pro nahrávání, aby záznam mohl zastavit:
try {
String serverCallId = call.getInfo().getServerCallId().get();
// Send serverCallId to your recording server to stop the call recording.
} catch (ExecutionException | InterruptedException e) {
} catch (UnsupportedOperationException unsupportedOperationException) {
}
Když zastavíte nahrávání ze serveru, událost handleCallOnIsRecordingChanged
se aktivuje a hodnota callRecordingFeature.isRecordingActive()
je false
.
Nastavení systému
Podle těchto kroků nastavte systém.
Vytvoření projektu Xcode
V Xcode vytvořte nový projekt pro iOS a vyberte šablonu aplikace s jedním zobrazením . Tento článek používá architekturu SwiftUI, takže byste měli nastavit jazyk na Swift a nastavit rozhraní na SwiftUI.
V tomto článku nebudete vytvářet testy. Zrušte zaškrtnutí políčka Zahrnout testy .
Instalace balíčku a závislostí pomocí CocoaPods
Vytvořte pro aplikaci soubor Podfile, například v tomto příkladu:
platform :ios, '13.0' use_frameworks! target 'AzureCommunicationCallingSample' do pod 'AzureCommunicationCalling', '~> 1.0.0' end
Spusťte
pod install
.Otevřete
.xcworkspace
pomocí Xcode.
Vyžádání přístupu k mikrofonu
Pokud chcete získat přístup k mikrofonu zařízení, musíte aktualizovat seznam vlastností informací aplikace pomocí NSMicrophoneUsageDescription
. Nastavte přidruženou hodnotu na řetězec, který je součástí dialogového okna, který systém používá k vyžádání přístupu od uživatele.
Klikněte pravým tlačítkem myši na položku Info.plist stromu projektu a pak vyberte Open As>Source Code. Do oddílu nejvyšší úrovně <dict>
přidejte následující řádky a pak soubor uložte.
<key>NSMicrophoneUsageDescription</key>
<string>Need microphone access for VOIP calling.</string>
Nastavení architektury aplikace
Otevřete soubor projektu ContentView.swift
. import
Přidejte deklaraci do horní části souboru pro import AzureCommunicationCalling
knihovny. Kromě toho import AVFoundation
. Potřebujete ho pro žádosti o zvuková oprávnění v kódu.
import AzureCommunicationCalling
import AVFoundation
Inicializace callagentu
Chcete-li vytvořit CallAgent
instanci z CallClient
, musíte použít metodu callClient.createCallAgent
, která asynchronně vrátí CallAgent
objekt po inicializaci.
Pokud chcete vytvořit klienta volání, předejte CommunicationTokenCredential
objekt:
import AzureCommunication
let tokenString = "token_string"
var userCredential: CommunicationTokenCredential?
do {
let options = CommunicationTokenRefreshOptions(initialToken: token, refreshProactively: true, tokenRefresher: self.fetchTokenSync)
userCredential = try CommunicationTokenCredential(withOptions: options)
} catch {
updates("Couldn't created Credential object", false)
initializationDispatchGroup!.leave()
return
}
// tokenProvider needs to be implemented by Contoso, which fetches a new token
public func fetchTokenSync(then onCompletion: TokenRefreshOnCompletion) {
let newToken = self.tokenProvider!.fetchNewToken()
onCompletion(newToken, nil)
}
CommunicationTokenCredential
Předejte objekt, který jste vytvořiliCallClient
, a nastavte zobrazovaný název:
self.callClient = CallClient()
let callAgentOptions = CallAgentOptions()
options.displayName = " iOS Azure Communication Services User"
self.callClient!.createCallAgent(userCredential: userCredential!,
options: callAgentOptions) { (callAgent, error) in
if error == nil {
print("Create agent succeeded")
self.callAgent = callAgent
} else {
print("Create agent failed")
}
})
Záznam hovorů
Poznámka:
Toto rozhraní API je poskytováno jako verze Preview pro vývojáře a může se měnit na základě zpětné vazby, kterou obdržíme. Toto rozhraní API nepoužívejte v produkčním prostředí. Pokud chcete toto rozhraní API použít, použijte beta verzi sady Azure Communication Services Call iOS SDK.
Záznam volání je rozšířená funkce základního Call
objektu.
Upozorňující
Až do verze 1.1.0 a beta verze 1.1.0-beta.1 sady Azure Communication Services Call iOS SDK byla isRecordingActive
součástí objektu Call
a didChangeRecordingState
byla součástí delegáta CallDelegate
. U nových beta verzí se tato rozhraní API přesunula jako rozšířená funkce Call
.
Nejprve potřebujete získat objekt funkce záznamu:
let callRecordingFeature = call.feature(Features.recording)
Potom zkontrolujte, jestli se hovor nahrává, zkontrolujte isRecordingActive
vlastnost callRecordingFeature
. Bool
Vrátí .
let isRecordingActive = callRecordingFeature.isRecordingActive;
Můžete se také přihlásit k odběru změn záznamu implementací delegáta RecordingCallFeatureDelegate
ve třídě s událostí didChangeRecordingState
:
callRecordingFeature.delegate = self
// didChangeRecordingState is a member of RecordingCallFeatureDelegate
public func recordingCallFeature(_ recordingCallFeature: RecordingCallFeature, didChangeRecordingState args: PropertyChangedEventArgs) {
let isRecordingActive = recordingFeature.isRecordingActive
}
Pokud chcete začít s nahráváním z aplikace, nejprve postupujte podle přehledu záznamu hovoru a nastavte záznam hovoru.
Po nastavení záznamu hovoru na serveru musíte z aplikace pro iOS získat ServerCallId
hodnotu z hovoru a pak ji odeslat na server, aby se spustil proces nahrávání. Hodnotu najdete ServerCallId
pomocí getServerCallId()
CallInfo
třídy. Třídu v objektu třídy můžete najít CallInfo
pomocí getInfo()
.
// Send serverCallId to your recording server to start the call recording.
let serverCallId = call.info.getServerCallId(){ (serverId, error) in }
Když začnete zaznamenávat ze serveru, událost didChangeRecordingState
se aktivuje a hodnota recordingFeature.isRecordingActive
je true
.
Stejně jako spuštění záznamu hovoru, pokud chcete záznam hovoru zastavit, musíte ho dostat ServerCallId
a odeslat na server pro nahrávání, aby záznam mohl zastavit:
// Send serverCallId to your recording server to stop the call recording.
let serverCallId = call.info.getServerCallId(){ (serverId, error) in }
Když zastavíte nahrávání ze serveru, událost didChangeRecordingState
se aktivuje a hodnota recordingFeature.isRecordingActive
je false
.
Nastavení systému
Podle těchto kroků nastavte systém.
Vytvoření projektu sady Visual Studio
V případě Univerzální platforma Windows aplikace vytvořte v sadě Visual Studio 2022 nový projekt Prázdná aplikace (Universal Windows). Po zadání názvu projektu si můžete vybrat libovolnou sadu Windows SDK později než 10.0.17763.0.
V případě aplikace WinUI 3 vytvořte nový projekt pomocí šablony Prázdná aplikace zabalená (WinUI 3 v desktopové verzi) pro nastavení jednostráňové aplikace WinUI 3. Vyžaduje se sada Windows App SDK verze 1.3 nebo novější.
Instalace balíčku a závislostí pomocí Správce balíčků NuGet
Rozhraní API a knihovny volající sady SDK jsou veřejně dostupné prostřednictvím balíčku NuGet.
Vyhledání, stažení a instalace balíčku NuGet volající sady SDK:
- Otevřete Správce balíčků NuGet výběrem nástrojů>NuGet Správce balíčků> Nabídky NuGet pro řešení.
- Vyberte Procházet a do vyhledávacího pole zadejte Azure.Communication.Calling.WindowsClient .
- Ujistěte se, že je zaškrtnuté políčko Zahrnout předběžné verze .
- Vyberte balíček Azure.Communication.Calling.WindowsClient a pak vyberte Azure.Communication.Calling.WindowsClient 1.4.0-beta.1 nebo novější verzi.
- Zaškrtněte políčko odpovídající projektu Azure Communication Services v pravém podokně.
- Vyberte volbu Instalovat.
Záznam hovorů
Záznam volání je rozšířená funkce základního Call
objektu. Nejprve potřebujete získat objekt funkce záznamu:
RecordingCallFeature recordingFeature = call.Features.Recording;
Potom zkontrolujte, jestli se hovor nahrává, zkontrolujte IsRecordingActive
vlastnost recordingFeature
. boolean
Vrátí .
boolean isRecordingActive = recordingFeature.IsRecordingActive;
Můžete se také přihlásit k odběru změn nahrávání:
private async void Call__OnIsRecordingActiveChanged(object sender, PropertyChangedEventArgs args)
boolean isRecordingActive = recordingFeature.IsRecordingActive;
}
recordingFeature.IsRecordingActiveChanged += Call__OnIsRecordingActiveChanged;
Záznam dodržování předpisů
Záznam dodržování předpisů je záznam, který je založený na zásadách Microsoft Teams. Můžete ho povolit pomocí tohoto kurzu: Úvod do záznamu založeného na zásadách Teams pro volání.
Záznam na základě zásad se spustí automaticky, když se uživatel, který má zásadu, připojí k hovoru. Pokud chcete získat oznámení o záznamu ze služby Azure Communication Services, použijte následující kód:
const callRecordingApi = call.feature(Features.Recording);
const isComplianceRecordingActive = callRecordingApi.isRecordingActive;
const isComplianceRecordingActiveChangedHandler = () => {
console.log(callRecordingApi.isRecordingActive);
};
callRecordingApi.on('isRecordingActiveChanged', isComplianceRecordingActiveChangedHandler);
Záznam dodržování předpisů můžete implementovat také pomocí vlastního robota pro nahrávání. Podívejte se na příklad GitHubu.