Teilen über


Lokale Ankerübertragungen in DirectX

In Situationen, in denen Sie Azure Spatial Anchors nicht verwenden können, ermöglichen lokale Ankerübertragungen ein HoloLens-Gerät, einen Anker zu exportieren, der von einem zweiten HoloLens-Gerät importiert wird.

Hinweis

Lokale Ankerübertragungen bieten weniger robuste Ankerrückrufe als Azure Spatial Anchors, und iOS- und Android-Geräte werden von diesem Ansatz nicht unterstützt.

Hinweis

Die Codeausschnitte in diesem Artikel veranschaulichen derzeit die Verwendung von C++/CX anstelle von C++17-kompatiblen C++/WinRT, wie sie in der C++-Projektvorlage für holografische Grafiken verwendet werden. Die Konzepte sind gleichwertig für ein C++/WinRT-Projekt, sie müssen den Code jedoch übersetzen.

Übertragen räumlicher Verankerungen

Sie können räumliche Verankerungen zwischen Windows Mixed Reality-Geräten mithilfe des SpatialAnchorTransferManager übertragen. Mit dieser API können Sie einen Anker mit allen unterstützenden Sensordaten bündeln, die erforderlich sind, um genau diesen Ort in der Welt zu finden, und dann dieses Bundle auf einem anderen Gerät importieren. Sobald die App auf dem zweiten Gerät diesen Anker importiert hat, kann jede App Hologramme mithilfe des koordinatensystems des freigegebenen räumlichen Ankers rendern, das dann an derselben Stelle in der realen Welt angezeigt wird.

Beachten Sie, dass räumliche Anker nicht zwischen verschiedenen Gerätetypen übertragen können, z. B. kann ein räumlicher HoloLens-Anker nicht mithilfe eines immersiven Headsets abgeschnitten werden. Übertragene Anker sind auch nicht mit iOS- oder Android-Geräten kompatibel.

Einrichten Der App für die Verwendung der spatialPerception-Funktion

Ihre App muss die Berechtigung zum Verwenden der SpatialPerception-Funktion erhalten, bevor sie den SpatialAnchorTransferManager verwenden kann. Dies ist notwendig, da die Übertragung eines räumlichen Ankers das Teilen von Sensorbildern umfasst, die im Laufe der Zeit in der Nähe dieses Ankers gesammelt wurden, was vertrauliche Informationen enthalten kann.

Deklarieren Sie diese Funktion in der Datei "package.appxmanifest" für Ihre App. Ein Beispiel:

<Capabilities>
  <uap2:Capability Name="spatialPerception" />
</Capabilities>

Die Funktion stammt aus dem uap2-Namespace . Um Zugriff auf diesen Namespace in Ihrem Manifest zu erhalten, fügen Sie ihn als xlmns-Attribut in das <Package-Element> ein. Ein Beispiel:

<Package
    xmlns="https://schemas.microsoft.com/appx/manifest/foundation/windows10"
    xmlns:mp="https://schemas.microsoft.com/appx/2014/phone/manifest"
    xmlns:uap="https://schemas.microsoft.com/appx/manifest/uap/windows10"
    xmlns:uap2="https://schemas.microsoft.com/appx/manifest/uap/windows10/2"
    IgnorableNamespaces="uap mp"
    >

HINWEIS: Ihre App muss die Funktion zur Laufzeit anfordern, bevor sie auf SpatialAnchor-Export-/Import-APIs zugreifen kann. Siehe RequestAccessAsync in den folgenden Beispielen.

Serialisieren von Ankerdaten durch Exportieren mit dem SpatialAnchorTransferManager

Eine Hilfsfunktion ist im Codebeispiel enthalten, um SpatialAnchor-Daten zu exportieren (serialisieren). Diese Export-API serialisiert alle Anker in einer Sammlung von Schlüsselwertpaaren, die Zeichenfolgen mit Ankern zuordnen.

// ExportAnchorDataAsync: Exports a byte buffer containing all of the anchors in the given collection.
//
// This function will place data in a buffer using a std::vector<byte>. The ata buffer contains one or more
// Anchors if one or more Anchors were successfully imported; otherwise, it is ot modified.
//
task<bool> SpatialAnchorImportExportHelper::ExportAnchorDataAsync(
    vector<byte>* anchorByteDataOut,
    IMap<String^, SpatialAnchor^>^ anchorsToExport
    )
{

Zunächst müssen wir den Datenstrom einrichten. Dies erlaubt uns 1.) Use TryExportAnchorsAsync to put the data in a buffer owned by the app, and 2.) Lesen von Daten aus dem exportierten Bytepufferdatenstrom , bei dem es sich um einen WinRT-Datenstrom handelt, in unseren eigenen Speicherpuffer, bei dem es sich um ein std::vector-Byte<> handelt.

// Create a random access stream to process the anchor byte data.
InMemoryRandomAccessStream^ stream = ref new InMemoryRandomAccessStream();
// Get an output stream for the anchor byte stream.
IOutputStream^ outputStream = stream->GetOutputStreamAt(0);

Wir müssen die Berechtigung für den Zugriff auf räumliche Daten anfordern, einschließlich Verankerungen, die vom System exportiert werden.

// Request access to spatial data.
auto accessRequestedTask = create_taskSpatialAnchorTransferManager::RequestAccessAsync()).then([anchorsToExport, utputStream](SpatialPerceptionAccessStatus status)
{
    if (status == SpatialPerceptionAccessStatus::Allowed)
    {
        // Access is allowed.
        // Export the indicated set of anchors.
        return create_task(SpatialAnchorTransferManager::TryExportAnchorsAsync(
            anchorsToExport,
            outputStream
            ));
    }
    else
    {
        // Access is denied.
        return task_from_result<bool>(false);
    }
});

Wenn wir Berechtigungen erhalten und Anker exportiert werden, können wir den Datenstrom lesen. Hier zeigen wir auch, wie Sie dataReader und InputStream erstellen, die wir zum Lesen der Daten verwenden.

// Get the input stream for the anchor byte stream.
IInputStream^ inputStream = stream->GetInputStreamAt(0);
// Create a DataReader, to get bytes from the anchor byte stream.
DataReader^ reader = ref new DataReader(inputStream);
return accessRequestedTask.then([anchorByteDataOut, stream, reader](bool nchorsExported)
{
    if (anchorsExported)
    {
        // Get the size of the exported anchor byte stream.
        size_t bufferSize = static_cast<size_t>(stream->Size);
        // Resize the output buffer to accept the data from the stream.
        anchorByteDataOut->reserve(bufferSize);
        anchorByteDataOut->resize(bufferSize);
        // Read the exported anchor store into the stream.
        return create_task(reader->LoadAsync(bufferSize));
    }
    else
    {
        return task_from_result<size_t>(0);
    }

Nachdem wir Bytes aus dem Datenstrom gelesen haben, können wir sie wie folgt in unserem eigenen Datenpuffer speichern.

}).then([anchorByteDataOut, reader](size_t bytesRead)
{
    if (bytesRead > 0)
    {
        // Read the bytes from the stream, into our data output buffer.
        reader->ReadBytes(Platform::ArrayReference<byte>(&(*anchorByteDataOut)[0], bytesRead));
        return true;
    }
    else
    {
        return false;
    }
});
};

Deserialisieren von Ankerdaten durch Importieren in das System mithilfe des SpatialAnchorTransferManager

Eine Hilfsfunktion ist im Codebeispiel enthalten, um zuvor exportierte Daten zu laden. Diese Deserialisierungsfunktion stellt eine Sammlung von Schlüssel-Wert-Paaren bereit, ähnlich wie der SpatialAnchorStore - mit der Ausnahme, dass wir diese Daten aus einer anderen Quelle erhalten haben, z. B. ein Netzwerksocket. Sie können diese Daten vor dem Speichern im Offlinemodus, im In-App-Speicher oder im SpatialAnchorStore Ihrer App verarbeiten und (falls zutreffend) speichern.

// ImportAnchorDataAsync: Imports anchors from a byte buffer that was previously exported.
//
// This function will import all anchors from a data buffer into an in-memory ollection of key, value
// pairs that maps String objects to SpatialAnchor objects. The Spatial nchorStore is not affected by
// this function unless you provide it as the target collection for import.
//
task<bool> SpatialAnchorImportExportHelper::ImportAnchorDataAsync(
    std::vector<byte>& anchorByteDataIn,
    IMap<String^, SpatialAnchor^>^ anchorMapOut
    )
{

Zunächst müssen wir Datenstromobjekte erstellen, um auf die Ankerdaten zuzugreifen. Wir schreiben die Daten aus unserem Puffer in einen Systempuffer. Daher erstellen wir einen DataWriter, der in einen In-Memory-Datenstrom schreibt, um unser Ziel zu erreichen, Verankerungen aus einem Bytepuffer als SpatialAnchors in das System zu übertragen.

// Create a random access stream for the anchor data.
InMemoryRandomAccessStream^ stream = ref new InMemoryRandomAccessStream();
// Get an output stream for the anchor data.
IOutputStream^ outputStream = stream->GetOutputStreamAt(0);
// Create a writer, to put the bytes in the stream.
DataWriter^ writer = ref new DataWriter(outputStream);

Erneut müssen wir sicherstellen, dass die App über die Berechtigung zum Exportieren räumlicher Ankerdaten verfügt, die private Informationen zur Umgebung des Benutzers enthalten können.

// Request access to transfer spatial anchors.
return create_task(SpatialAnchorTransferManager::RequestAccessAsync()).then(
    [&anchorByteDataIn, writer](SpatialPerceptionAccessStatus status)
{
    if (status == SpatialPerceptionAccessStatus::Allowed)
    {
        // Access is allowed.

Wenn der Zugriff zulässig ist, können wir Bytes aus dem Puffer in einen Systemdatenstrom schreiben.

// Write the bytes to the stream.
        byte* anchorDataFirst = &anchorByteDataIn[0];
        size_t anchorDataSize = anchorByteDataIn.size();
        writer->WriteBytes(Platform::ArrayReference<byte>(anchorDataFirst, anchorDataSize));
        // Store the stream.
        return create_task(writer->StoreAsync());
    }
    else
    {
        // Access is denied.
        return task_from_result<size_t>(0);
    }

Wenn die Speicherung von Bytes im Datenstrom erfolgreich war, können wir versuchen, diese Daten mithilfe des SpatialAnchorTransferManager zu importieren.

}).then([writer, stream](unsigned int bytesWritten)
{
    if (bytesWritten > 0)
    {
        // Try to import anchors from the byte stream.
        return create_task(writer->FlushAsync())
            .then([stream](bool dataWasFlushed)
        {
            if (dataWasFlushed)
            {
                // Get the input stream for the anchor data.
                IInputStream^ inputStream = stream->GetInputStreamAt(0);
                return create_task(SpatialAnchorTransferManager::TryImportAnchorsAsync(inputStream));
            }
            else
            {
                return task_from_result<IMapView<String^, SpatialAnchor^>^>(nullptr);
            }
        });
    }
    else
    {
        return task_from_result<IMapView<String^, SpatialAnchor^>^>(nullptr);
    }

Wenn die Daten importiert werden können, erhalten wir eine Kartenansicht von Schlüsselwertpaaren, die Zeichenfolgen mit Ankern zuordnen. Wir können dies in unsere eigene Speicherdatensammlung laden und diese Sammlung verwenden, um nach Anker zu suchen, die wir verwenden möchten.

}).then([anchorMapOut](task<Windows::Foundation::Collections::IMapView<String^, SpatialAnchor^>^>  previousTask)
{
    try
    {
        auto importedAnchorsMap = previousTask.get();
        // If the operation was successful, we get a set of imported anchors.
        if (importedAnchorsMap != nullptr)
        {
            for each (auto& pair in importedAnchorsMap)
            {
                // Note that you could look for specific anchors here, if you know their key values.
                auto const& id = pair->Key;
                auto const& anchor = pair->Value;
                // Append "Remote" to the end of the anchor name for disambiguation.
                std::wstring idRemote(id->Data());
                idRemote += L"Remote";
                String^ idRemoteConst = ref new String (idRemote.c_str());
                // Store the anchor in the current in-memory anchor map.
                anchorMapOut->Insert(idRemoteConst, anchor);
            }
            return true;
        }
    }
    catch (Exception^ exception)
    {
        OutputDebugString(L"Error: Unable to import the anchor data buffer bytes into the in-memory anchor collection.\n");
    }
    return false;
});
}

HINWEIS: Nur weil Sie einen Anker importieren können, bedeutet dies nicht unbedingt, dass Sie ihn sofort verwenden können. Der Anker befindet sich möglicherweise in einem anderen Raum oder an einem anderen physischen Ort vollständig; der Anker kann erst abgeschnitten werden, wenn das empfangene Gerät über genügend visuelle Informationen zur Umgebung verfügt, in der der Anker erstellt wurde, um die Position des Ankers relativ zur bekannten aktuellen Umgebung wiederherzustellen. Die Clientimplementierung sollte versuchen, den Anker relativ zu Ihrem lokalen Koordinatensystem oder Referenzframe zu suchen, bevor Sie versuchen, ihn für Liveinhalte zu verwenden. Versuchen Sie beispielsweise, den Anker relativ zu einem aktuellen Koordinatensystem regelmäßig zu suchen, bis der Anker ablösbar ist.

Besondere Überlegungen

Mit der TryExportAnchorsAsync-API können mehrere SpatialAnchors in das gleiche undurchsichtige binäre Blob exportiert werden. Es gibt jedoch einen subtilen Unterschied darin, welche Daten das Blob enthalten soll, je nachdem, ob ein einzelner SpatialAnchor oder mehrere SpatialAnchors in einem einzigen Aufruf exportiert werden.

Exportieren eines einzelnen SpatialAnchor

Das Blob enthält eine Darstellung der Umgebung in der Nähe des SpatialAnchor, sodass die Umgebung auf dem Gerät erkannt werden kann, das den SpatialAnchor importiert. Nach Abschluss des Imports ist der neue SpatialAnchor für das Gerät verfügbar. Vorausgesetzt, der Benutzer befindet sich vor kurzem in der Nähe des Ankers, kann er abgeschnitten werden, und Hologramme, die mit dem SpatialAnchor verbunden sind, können gerendert werden. Diese Hologramme werden an derselben physischen Position angezeigt, die sie auf dem ursprünglichen Gerät ausgeführt haben, das den SpatialAnchor exportiert hat.

Exportieren eines einzelnen SpatialAnchor

Export mehrerer SpatialAnchors

Wie beim Export eines einzelnen SpatialAnchor enthält das Blob eine Darstellung der Umgebung in der Nähe aller angegebenen SpatialAnchors. Darüber hinaus enthält das Blob Informationen zu den Verbindungen zwischen den enthaltenen SpatialAnchors, wenn sie sich im selben physischen Raum befinden. Dies bedeutet, dass, wenn zwei räumlicheAnchors in der Nähe importiert werden, ein Hologramm, das an den zweiten SpatialAnchor angefügt ist, ablösbar wäre, auch wenn das Gerät die Umgebung nur um den ersten SpatialAnchor erkennt, da genügend Daten zum Berechnen der Transformation zwischen den beiden SpatialAnchors im Blob enthalten waren. Wenn die beiden SpatialAnchors einzeln exportiert wurden (zwei separate Aufrufe an TryExportSpatialAnchors), sind möglicherweise nicht genügend Daten im Blob enthalten, damit Hologramme, die an den zweiten SpatialAnchor angefügt sind, ablösbar sind, wenn sich das erste befindet.

Mehrere Anker, die mit einem einzelnen TryExportAnchorsAsync-Aufruf exportiert wurden Mehrere Anker, die mit einem separaten TryExportAnchorsAsync-Aufruf für jeden Anker exportiert wurden

Beispiel: Senden von Ankerdaten mithilfe eines Windows::Networking::StreamSocket

Hier finden Sie ein Beispiel für die Verwendung exportierter Ankerdaten durch Senden über ein TCP-Netzwerk. Dies stammt aus dem HolographicSpatialAnchorTransferSample.

Die WinRT StreamSocket-Klasse verwendet die PPL-Aufgabenbibliothek. Bei Netzwerkfehlern wird der Fehler mithilfe einer erneut ausgelösten Ausnahme an die nächste Aufgabe in der Kette zurückgegeben. Die Ausnahme enthält ein HRESULT, das den Fehlerstatus angibt.

Verwenden eines Windows::Networking::StreamSocketListener mit TCP zum Senden exportierter Ankerdaten

Erstellen Sie eine Serverinstanz, die auf eine Verbindung lauscht.

void SampleAnchorTcpServer::ListenForConnection()
{
    // Make a local copy to avoid races with Closed events.
    StreamSocketListener^ streamSocketListener = m_socketServer;
    if (streamSocketListener == nullptr)
    {
        OutputDebugString(L"Server listening for client.\n");
        // Create the web socket connection.
        streamSocketListener = ref new StreamSocketListener();
        streamSocketListener->Control->KeepAlive = true;
        streamSocketListener->BindEndpointAsync(
            SampleAnchorTcpCommon::m_serverHost,
            SampleAnchorTcpCommon::m_tcpPort
            );
        streamSocketListener->ConnectionReceived +=
            ref new Windows::Foundation::TypedEventHandler<StreamSocketListener^, StreamSocketListenerConnectionReceivedEventArgs^>(
                std::bind(&SampleAnchorTcpServer::OnConnectionReceived, this, _1, _2)
                );
        m_socketServer = streamSocketListener;
    }
    else
    {
        OutputDebugString(L"Error: Stream socket listener not created.\n");
    }
}

Wenn eine Verbindung empfangen wird, verwenden Sie die Clientsocketverbindung, um Ankerdaten zu senden.

void SampleAnchorTcpServer::OnConnectionReceived(StreamSocketListener^ listener, StreamSocketListenerConnectionReceivedEventArgs^ args)
{
    m_socketForClient = args->Socket;
    if (m_socketForClient != nullptr)
    {
        // In this example, when the client first connects, we catch it up to the current state of our anchor set.
        OutputToClientSocket(m_spatialAnchorHelper->GetAnchorMap());
    }
}

Jetzt können wir beginnen, einen Datenstrom zu senden, der die exportierten Ankerdaten enthält.

void SampleAnchorTcpServer::OutputToClientSocket(IMap<String^, SpatialAnchor^>^ anchorsToSend)
{
    m_anchorTcpSocketStreamWriter = ref new DataWriter(m_socketForClient->OutputStream);
    OutputDebugString(L"Sending stream to client.\n");
    SendAnchorDataStream(anchorsToSend).then([this](task<bool> previousTask)
    {
        try
        {
            bool success = previousTask.get();
            if (success)
            {
                OutputDebugString(L"Anchor data sent!\n");
            }
            else
            {
                OutputDebugString(L"Error: Anchor data not sent.\n");
            }
        }
        catch (Exception^ exception)
        {
            HandleException(exception);
            OutputDebugString(L"Error: Anchor data was not sent.\n");
        }
    });
}

Bevor wir den Datenstrom selbst senden können, müssen wir zuerst ein Headerpaket senden. Dieses Headerpaket muss eine feste Länge aufweisen, und es muss auch die Länge des variablen Arrays von Bytes angeben, die der Ankerdatenstrom ist. Im Fall dieses Beispiels sind keine anderen Headerdaten zu senden, sodass der Header 4 Byte lang ist und eine 32-Bit-ganzzahl ohne Vorzeichen enthält.

Concurrency::task<bool> SampleAnchorTcpServer::SendAnchorDataLengthMessage(size_t dataStreamLength)
{
    unsigned int arrayLength = dataStreamLength;
    byte* data = reinterpret_cast<byte*>(&arrayLength);
    m_anchorTcpSocketStreamWriter->WriteBytes(Platform::ArrayReference<byte>(data, SampleAnchorTcpCommon::c_streamHeaderByteArrayLength));
    return create_task(m_anchorTcpSocketStreamWriter->StoreAsync()).then([this](unsigned int bytesStored)
    {
        if (bytesStored > 0)
        {
            OutputDebugString(L"Anchor data length stored in stream; Flushing stream.\n");
            return create_task(m_anchorTcpSocketStreamWriter->FlushAsync());
        }
        else
        {
            OutputDebugString(L"Error: Anchor data length not stored in stream.\n");
            return task_from_result<bool>(false);
        }
    });
}
Concurrency::task<bool> SampleAnchorTcpServer::SendAnchorDataStreamIMap<String^, SpatialAnchor^>^ anchorsToSend)
{
    return SpatialAnchorImportExportHelper::ExportAnchorDataAsync(
        &m_exportedAnchorStoreBytes,
        anchorsToSend
        ).then([this](bool anchorDataExported)
    {
        if (anchorDataExported)
        {
            const size_t arrayLength = m_exportedAnchorStoreBytes.size();
            if (arrayLength > 0)
            {
                OutputDebugString(L"Anchor data was exported; sending data stream length message.\n");
                return SendAnchorDataLengthMessage(arrayLength);
            }
        }
        OutputDebugString(L"Error: Anchor data was not exported.\n");
        // No data to send.
        return task_from_result<bool>(false);

Nachdem die Datenstromlänge in Bytes an den Client gesendet wurde, können wir mit dem Schreiben des Datenstroms selbst in den Socketdatenstrom fortfahren. Dies führt dazu, dass die Ankerspeicherbytes an den Client gesendet werden.

}).then([this](bool dataLengthSent)
    {
        if (dataLengthSent)
        {
            OutputDebugString(L"Data stream length message sent; writing exported anchor store bytes to stream.\n");
            m_anchorTcpSocketStreamWriter->WriteBytes(Platform::ArrayReference<byte>(&m_exportedAnchorStoreBytes[0], m_exportedAnchorStoreBytes.size()));
            return create_task(m_anchorTcpSocketStreamWriter->StoreAsync());
        }
        else
        {
            OutputDebugString(L"Error: Data stream length message not sent.\n");
            return task_from_result<size_t>(0);
        }
    }).then([this](unsigned int bytesStored)
    {
        if (bytesStored > 0)
        {
            PrintWstringToDebugConsole(
                std::to_wstring(bytesStored) +
                L" bytes of anchor data written and stored to stream; flushing stream.\n"
                );
        }
        else
        {
            OutputDebugString(L"Error: No anchor data bytes were written to the stream.\n");
        }
        return task_from_result<bool>(false);
    });
}

Wie weiter oben in diesem Thema erwähnt, müssen wir darauf vorbereitet sein, Ausnahmen mit Netzwerkfehlermeldungen zu behandeln. Bei Fehlern, die nicht erwartet werden, können wir die Ausnahmeinformationen wie folgt in die Debugkonsole schreiben. Dies gibt uns einen Hinweis darauf, was passiert ist, wenn unser Codebeispiel die Verbindung nicht abschließen kann oder wenn das Senden der Ankerdaten nicht abgeschlossen werden kann.

void SampleAnchorTcpServer::HandleException(Exception^ exception)
{
    PrintWstringToDebugConsole(
        std::wstring(L"Connection error: ") +
        exception->ToString()->Data() +
        L"\n"
        );
}

Verwenden eines Windows::Networking::StreamSocket mit TCP zum Empfangen exportierter Ankerdaten

Zunächst müssen wir eine Verbindung mit dem Server herstellen. In diesem Codebeispiel wird gezeigt, wie Sie einen StreamSocket erstellen und konfigurieren und einen DataReader erstellen, mit dem Sie Netzwerkdaten mithilfe der Socketverbindung abrufen können.

HINWEIS: Wenn Sie diesen Beispielcode ausführen, stellen Sie sicher, dass Sie den Server konfigurieren und starten, bevor Sie den Client starten.

task<bool> SampleAnchorTcpClient::ConnectToServer()
{
    // Make a local copy to avoid races with Closed events.
    StreamSocket^ streamSocket = m_socketClient;
    // Have we connected yet?
    if (m_socketClient == nullptr)
    {
        OutputDebugString(L"Client is attempting to connect to server.\n");
        EndpointPair^ endpointPair = ref new EndpointPair(
            SampleAnchorTcpCommon::m_clientHost,
            SampleAnchorTcpCommon::m_tcpPort,
            SampleAnchorTcpCommon::m_serverHost,
            SampleAnchorTcpCommon::m_tcpPort
            );
        // Create the web socket connection.
        m_socketClient = ref new StreamSocket();
        // The client connects to the server.
        return create_task(m_socketClient->ConnectAsync(endpointPair, SocketProtectionLevel::PlainSocket)).then([this](task<void> previousTask)
        {
            try
            {
                // Try getting all exceptions from the continuation chain above this point.
                previousTask.get();
                m_anchorTcpSocketStreamReader = ref new DataReader(m_socketClient->InputStream);
                OutputDebugString(L"Client connected!\n");
                m_anchorTcpSocketStreamReader->InputStreamOptions = InputStreamOptions::ReadAhead;
                WaitForAnchorDataStream();
                return true;
            }
            catch (Exception^ exception)
            {
                if (exception->HResult == 0x80072741)
                {
                    // This code sample includes a very simple implementation of client/server
                    // endpoint detection: if the current instance tries to connect to itself,
                    // it is determined to be the server.
                    OutputDebugString(L"Starting up the server instance.\n");
                    // When we return false, we'll start up the server instead.
                    return false;
                }
                else if ((exception->HResult == 0x8007274c) || // connection timed out
                    (exception->HResult == 0x80072740)) // connection maxed at server end
                {
                    // If the connection timed out, try again.
                    ConnectToServer();
                }
                else if (exception->HResult == 0x80072741)
                {
                    // No connection is possible.
                }
                HandleException(exception);
                return true;
            }
        });
    }
    else
    {
        OutputDebugString(L"A StreamSocket connection to a server already exists.\n");
        return task_from_result<bool>(true);
    }
}

Sobald eine Verbindung hergestellt wurde, können wir warten, bis der Server Daten sendet. Dazu rufen wir LoadAsync für den Datenstromleser auf.

Der erste satz von Bytes, die wir empfangen, sollte immer das Headerpaket sein, das die Länge des Ankerdatenstrombytes angibt, wie im vorherigen Abschnitt beschrieben.

void SampleAnchorTcpClient::WaitForAnchorDataStream()
{
    if (m_anchorTcpSocketStreamReader == nullptr)
    {
        // We have not connected yet.
        return;
    }
    OutputDebugString(L"Waiting for server message.\n");
    // Wait for the first message, which specifies the byte length of the string data.
    create_task(m_anchorTcpSocketStreamReader->LoadAsync(SampleAnchorTcpCommon::c_streamHeaderByteArrayLength)).then([this](unsigned int numberOfBytes)
    {
        if (numberOfBytes > 0)
        {
            OutputDebugString(L"Server message incoming.\n");
            return ReceiveAnchorDataLengthMessage();
        }
        else
        {
            OutputDebugString(L"0-byte async task received, awaiting server message again.\n");
            WaitForAnchorDataStream();
            return task_from_result<size_t>(0);
        }

...

task<size_t> SampleAnchorTcpClient::ReceiveAnchorDataLengthMessage()
{
    byte data[4];
    m_anchorTcpSocketStreamReader->ReadBytes(Platform::ArrayReference<byte>(data, SampleAnchorTcpCommon::c_streamHeaderByteArrayLength));
    unsigned int lengthMessageSize = *reinterpret_cast<unsigned int*>(data);
    if (lengthMessageSize > 0)
    {
        OutputDebugString(L"One or more anchors to be received.\n");
        return task_from_result<size_t>(lengthMessageSize);
    }
    else
    {
        OutputDebugString(L"No anchors to be received.\n");
        ConnectToServer();
    }
    return task_from_result<size_t>(0);
}

Nachdem wir das Headerpaket erhalten haben, wissen wir, wie viele Bytes von Ankerdaten wir erwarten sollten. Wir können diese Bytes aus dem Datenstrom lesen.

}).then([this](size_t dataStreamLength)
    {
        if (dataStreamLength > 0)
        {
            std::wstring debugMessage = std::to_wstring(dataStreamLength);
            debugMessage += L" bytes of anchor data incoming.\n";
            OutputDebugString(debugMessage.c_str());
            // Prepare to receive the data stream in one or more pieces.
            m_anchorStreamLength = dataStreamLength;
            m_exportedAnchorStoreBytes.clear();
            m_exportedAnchorStoreBytes.resize(m_anchorStreamLength);
            OutputDebugString(L"Loading byte stream.\n");
            return ReceiveAnchorDataStream();
        }
        else
        {
            OutputDebugString(L"Error: Anchor data size not received.\n");
            ConnectToServer();
            return task_from_result<bool>(false);
        }
    });
}

Hier sehen Sie unseren Code zum Empfangen des Ankerdatenstroms. Auch hier werden die Bytes zuerst aus dem Datenstrom geladen. Dieser Vorgang kann einige Zeit in Anspruch nehmen, da das StreamSocket wartet, um diese Bytemenge aus dem Netzwerk zu empfangen.

Wenn der Ladevorgang abgeschlossen ist, können wir diese Anzahl von Bytes lesen. Wenn wir die Anzahl der Bytes erhalten haben, die wir für den Ankerdatenstrom erwarten, können wir fortfahren und die Ankerdaten importieren. wenn nicht, muss eine Art von Fehler aufgetreten sein. Dies kann z. B. passieren, wenn die Serverinstanz beendet wird, bevor das Senden des Datenstroms abgeschlossen werden kann, oder das Netzwerk geht nach unten, bevor der gesamte Datenstrom vom Client empfangen werden kann.

task<bool> SampleAnchorTcpClient::ReceiveAnchorDataStream()
{
    if (m_anchorStreamLength > 0)
    {
        // First, we load the bytes from the network socket.
        return create_task(m_anchorTcpSocketStreamReader->LoadAsync(m_anchorStreamLength)).then([this](size_t bytesLoadedByStreamReader)
        {
            if (bytesLoadedByStreamReader > 0)
            {
                // Once the bytes are loaded, we can read them from the stream.
                m_anchorTcpSocketStreamReader->ReadBytes(Platform::ArrayReference<byte>(&m_exportedAnchorStoreBytes[0],
                    bytesLoadedByStreamReader));
                // Check status.
                if (bytesLoadedByStreamReader == m_anchorStreamLength)
                {
                    // The whole stream has arrived. We can process the data.
                    // Informational message of progress complete.
                    std::wstring infoMessage = std::to_wstring(bytesLoadedByStreamReader);
                    infoMessage += L" bytes read out of ";
                    infoMessage += std::to_wstring(m_anchorStreamLength);
                    infoMessage += L" total bytes; importing the data.\n";
                    OutputDebugStringW(infoMessage.c_str());
                    // Kick off a thread to wait for a new message indicating another incoming anchor data stream.
                    WaitForAnchorDataStream();
                    // Process the data for the stream we just received.
                    return SpatialAnchorImportExportHelper::ImportAnchorDataAsync(m_exportedAnchorStoreBytes, m_spatialAnchorHelper->GetAnchorMap());
                }
                else
                {
                    OutputDebugString(L"Error: Fewer than expected anchor data bytes were received.\n");
                }
            }
            else
            {
                OutputDebugString(L"Error: No anchor bytes were received.\n");
            }
            return task_from_result<bool>(false);
        });
    }
    else
    {
        OutputDebugString(L"Warning: A zero-length data buffer was sent.\n");
        return task_from_result<bool>(false);
    }
}

Auch hier müssen wir bereit sein, unbekannte Netzwerkfehler zu behandeln.

void SampleAnchorTcpClient::HandleException(Exception^ exception)
{
    std::wstring error = L"Connection error: ";
    error += exception->ToString()->Data();
    error += L"\n";
    OutputDebugString(error.c_str());
}

Das ist alles! Jetzt sollten Sie über genügend Informationen verfügen, um zu versuchen, die über das Netzwerk empfangenen Anker zu finden. Beachten Sie erneut, dass der Client über genügend visuelle Tracking-Daten verfügen muss, damit der Platz erfolgreich gefunden werden kann. Wenn es nicht sofort funktioniert, versuchen Sie, eine Weile herumzulaufen. Wenn es immer noch nicht funktioniert, senden Sie dem Server weitere Anker und verwenden Sie die Netzwerkkommunikation, um sich auf einen zu einigen, der für den Client funktioniert. Sie können dies ausprobieren, indem Sie das HolographicSpatialAnchorTransferSample herunterladen, Ihren Client- und Server-IPs konfigurieren und auf Client- und Server-HoloLens-Geräten bereitstellen.

Weitere Informationen